compass-kb-validation 3.2.1__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.2.1 → compass_kb_validation-3.4.0}/PKG-INFO +15 -1
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/README.md +14 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/__init__.py +1 -1
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/citation.py +50 -1
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/cli.py +158 -0
- compass_kb_validation-3.4.0/compass/eval_enrich.py +380 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/grounding.py +5 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/PKG-INFO +15 -1
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/SOURCES.txt +2 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_citation.py +29 -0
- compass_kb_validation-3.4.0/tests/test_eval_enrich.py +135 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/LICENSE +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/__main__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/ask.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/baseline.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/chunk_coherence.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/config/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/config_lint.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/content_inspect.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/corpus.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/corpus_health.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/diagnostics.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/embedding_drift.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/eval_interop.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/evidence.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/init_wizard.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/live_dashboard.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/mcp_server.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/models/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/notifications.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/pipeline/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/profiler.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/provenance.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/embedding.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/graph.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/source.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/storage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/providers/vectorstore.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/remediation.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/_report_css.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/_report_js.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/analysis.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/audit.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/export.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/report/shell.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/retrieval_analysis.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/text_probes.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/trend_tracker.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/utils/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/api_upload.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/base.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/base_sparql.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/cross_stage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/graphdb_metadata.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/neptune_metadata.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/redis_chunks.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/registry.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/retrieval_quality.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass/validators/s3_storage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/requires.txt +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/discover_and_validate.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/ingestion_validation/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/profile_kb.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/pyproject.toml +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/run_validation.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/setup.cfg +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_alt_adapters.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_analysis_report.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_api_upload.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_ask.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_baseline.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_benchmark.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_check_catalog.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_cli.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_compat_shim.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_config.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_config_lint.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_content_inspect.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_corpus.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_corpus_health.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_coverage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_cross_stage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_diagnostics.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_discover_cli.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_discovery_parallel.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_embedding_drift.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_eval_interop.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_evidence.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_gate.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_graph_transport.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_graph_validators.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_grounding.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_init_wizard.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_manifest.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_mcp.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_migration.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_models.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_neptune_bugfixes.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_neptune_bugfixes2.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_notifications.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_pipeline.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_profiler.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_providers.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_redis_chunks.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_registry.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_remediation.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_reporting.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_retrieval_assertions.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_retrieval_quality.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_review_hardening.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_run_cli.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_sources.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_storage_validator.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_trends.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_universal_concerns.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_utils.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.4.0}/tests/test_vectorstore_filters.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compass-kb-validation
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Knowledge-base readiness validation for RAG & AI agents — config-driven, deterministic pipeline integrity checks.
|
|
5
5
|
Author: COMPASS
|
|
6
6
|
License: MIT
|
|
@@ -147,6 +147,7 @@ Two modes: **live** validation as documents are ingested, and **post-hoc discove
|
|
|
147
147
|
✅ **Pluggable steps** — register custom validators in a step registry; the report adapts automatically
|
|
148
148
|
✅ **Deterministic & cheap** — structural/byte/schema checks, no LLM, CI-gateable
|
|
149
149
|
✅ **Query & analyze the KB** — ask it questions, expose it to agents over MCP, and preview embedding-model migrations / map retrieval coverage — all deterministic, read-only (see *Beyond validation*)
|
|
150
|
+
✅ **Eval oracle** — enrich any eval with deterministic KB-grounding signals, then measure how accurate its LLM judges are against that ground truth (`compass enrich` / `compass calibrate-judges`) — the eval framework guesses, COMPASS checks the KB
|
|
150
151
|
✅ **Interactive reporting** — self-contained HTML dashboard + JSON + JUnit XML
|
|
151
152
|
✅ **Trend Tracking** — SQLite historical performance analysis
|
|
152
153
|
✅ **Notifications** — Slack/Teams webhooks for alerts
|
|
@@ -238,8 +239,21 @@ compass freshness --project my-kb --stale-after 30
|
|
|
238
239
|
# chunks when present (Level 1) and the live KB (Level 2); the answer text + a reachable KB alone
|
|
239
240
|
# still catches fabricated figures and phantom citations.
|
|
240
241
|
compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
|
|
242
|
+
|
|
243
|
+
# Eval oracle (deterministic, no LLM): enrich an eval with KB-grounded signals, then measure how
|
|
244
|
+
# accurate the eval's OWN LLM judges are against that deterministic truth. LLM judges GUESS whether
|
|
245
|
+
# a response is grounded; COMPASS CHECKS it against the KB - so it can tell you which judge is right.
|
|
246
|
+
compass enrich --project my-kb --input eval.json --output enriched.json
|
|
247
|
+
compass calibrate-judges --input enriched.json --metric groundedness
|
|
241
248
|
```
|
|
242
249
|
|
|
250
|
+
`enrich` adds a `compass` block per case (a **citation grounding rate** over the verifiable
|
|
251
|
+
citation+numeric subset, citation fidelity, chunk quality, context utilization, `COMPASS_*` signal
|
|
252
|
+
codes). It is **schema-agnostic** — every field resolves through your `eval.gt_*` config, so it
|
|
253
|
+
works whatever shape your eval JSON takes. `calibrate-judges` then reports each judge's error,
|
|
254
|
+
bias, accuracy, and an evidence-based **recommended weight** — something no eval framework can
|
|
255
|
+
compute, because it needs KB access.
|
|
256
|
+
|
|
243
257
|
`migrate` and `coverage` also render a **self-contained HTML report** (same look as the
|
|
244
258
|
validation dashboard) with `--html [path] --open` — a coverage heatmap, a migration
|
|
245
259
|
stability distribution, verdicts, and the most-changed / dead-zone tables.
|
|
@@ -80,6 +80,7 @@ Two modes: **live** validation as documents are ingested, and **post-hoc discove
|
|
|
80
80
|
✅ **Pluggable steps** — register custom validators in a step registry; the report adapts automatically
|
|
81
81
|
✅ **Deterministic & cheap** — structural/byte/schema checks, no LLM, CI-gateable
|
|
82
82
|
✅ **Query & analyze the KB** — ask it questions, expose it to agents over MCP, and preview embedding-model migrations / map retrieval coverage — all deterministic, read-only (see *Beyond validation*)
|
|
83
|
+
✅ **Eval oracle** — enrich any eval with deterministic KB-grounding signals, then measure how accurate its LLM judges are against that ground truth (`compass enrich` / `compass calibrate-judges`) — the eval framework guesses, COMPASS checks the KB
|
|
83
84
|
✅ **Interactive reporting** — self-contained HTML dashboard + JSON + JUnit XML
|
|
84
85
|
✅ **Trend Tracking** — SQLite historical performance analysis
|
|
85
86
|
✅ **Notifications** — Slack/Teams webhooks for alerts
|
|
@@ -171,8 +172,21 @@ compass freshness --project my-kb --stale-after 30
|
|
|
171
172
|
# chunks when present (Level 1) and the live KB (Level 2); the answer text + a reachable KB alone
|
|
172
173
|
# still catches fabricated figures and phantom citations.
|
|
173
174
|
compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
|
|
175
|
+
|
|
176
|
+
# Eval oracle (deterministic, no LLM): enrich an eval with KB-grounded signals, then measure how
|
|
177
|
+
# accurate the eval's OWN LLM judges are against that deterministic truth. LLM judges GUESS whether
|
|
178
|
+
# a response is grounded; COMPASS CHECKS it against the KB - so it can tell you which judge is right.
|
|
179
|
+
compass enrich --project my-kb --input eval.json --output enriched.json
|
|
180
|
+
compass calibrate-judges --input enriched.json --metric groundedness
|
|
174
181
|
```
|
|
175
182
|
|
|
183
|
+
`enrich` adds a `compass` block per case (a **citation grounding rate** over the verifiable
|
|
184
|
+
citation+numeric subset, citation fidelity, chunk quality, context utilization, `COMPASS_*` signal
|
|
185
|
+
codes). It is **schema-agnostic** — every field resolves through your `eval.gt_*` config, so it
|
|
186
|
+
works whatever shape your eval JSON takes. `calibrate-judges` then reports each judge's error,
|
|
187
|
+
bias, accuracy, and an evidence-based **recommended weight** — something no eval framework can
|
|
188
|
+
compute, because it needs KB access.
|
|
189
|
+
|
|
176
190
|
`migrate` and `coverage` also render a **self-contained HTML report** (same look as the
|
|
177
191
|
validation dashboard) with `--html [path] --open` — a coverage heatmap, a migration
|
|
178
192
|
stability distribution, verdicts, and the most-changed / dead-zone tables.
|
|
@@ -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),
|
|
@@ -1701,6 +1701,158 @@ def cmd_selftest(argv: list[str]) -> int:
|
|
|
1701
1701
|
# --------------------------------------------------------------------------- #
|
|
1702
1702
|
# Dispatcher
|
|
1703
1703
|
# --------------------------------------------------------------------------- #
|
|
1704
|
+
def cmd_enrich(argv: list[str]) -> int:
|
|
1705
|
+
p = argparse.ArgumentParser(
|
|
1706
|
+
prog="compass enrich",
|
|
1707
|
+
description="Add deterministic, KB-derived signals to an eval file: a citation grounding "
|
|
1708
|
+
"rate (over the verifiable citation+numeric subset), citation fidelity, chunk "
|
|
1709
|
+
"quality, context utilization, and COMPASS_* signal codes. Read-only, no LLM. "
|
|
1710
|
+
"Schema-agnostic - field locations come from your eval.gt_* config.")
|
|
1711
|
+
p.add_argument("--project")
|
|
1712
|
+
p.add_argument("--env", default="")
|
|
1713
|
+
p.add_argument("--input", required=True, metavar="FILE",
|
|
1714
|
+
help="Eval file: a JSON array of cases, or a JSON object carrying a case array")
|
|
1715
|
+
p.add_argument("--output", metavar="FILE", help="Write enriched JSON here (default: stdout)")
|
|
1716
|
+
p.add_argument("--no-retrieval", action="store_true",
|
|
1717
|
+
help="Skip live-KB (Level 2) verification; use only the eval's own chunks "
|
|
1718
|
+
"(Level 1). No backend needed.")
|
|
1719
|
+
p.add_argument("--json", action="store_true", help="(default) JSON output")
|
|
1720
|
+
args = p.parse_args(argv)
|
|
1721
|
+
_health_env(args)
|
|
1722
|
+
|
|
1723
|
+
import json as _json
|
|
1724
|
+
|
|
1725
|
+
from compass.config import load_settings, project_config_file
|
|
1726
|
+
from compass.eval_enrich import discover_cases, enrich_cases
|
|
1727
|
+
|
|
1728
|
+
if args.project and project_config_file(args.project) is None:
|
|
1729
|
+
print(f" Project '{args.project}' not found (config/projects/{args.project}.yaml).", file=sys.stderr)
|
|
1730
|
+
return 2
|
|
1731
|
+
try:
|
|
1732
|
+
settings = load_settings(project=args.project, env=args.env or None)
|
|
1733
|
+
except Exception as exc: # noqa: BLE001 - clean message, not a traceback
|
|
1734
|
+
print(f" Config error: {exc}", file=sys.stderr)
|
|
1735
|
+
return 2
|
|
1736
|
+
try:
|
|
1737
|
+
with open(args.input, encoding="utf-8") as f:
|
|
1738
|
+
data = _json.load(f)
|
|
1739
|
+
except (OSError, ValueError) as exc:
|
|
1740
|
+
print(f" Cannot read {args.input}: {exc}", file=sys.stderr)
|
|
1741
|
+
return 2
|
|
1742
|
+
cases = discover_cases(data)
|
|
1743
|
+
if not cases:
|
|
1744
|
+
print(f" No case array found in {args.input}.", file=sys.stderr)
|
|
1745
|
+
return 2
|
|
1746
|
+
|
|
1747
|
+
# A reachable KB enables Level 2 (KB-verified) attribution; without it, Level 1 signals from
|
|
1748
|
+
# the eval's own chunks still compute. Never fail the command over an unreachable KB.
|
|
1749
|
+
adapter = None
|
|
1750
|
+
if args.project and not args.no_retrieval:
|
|
1751
|
+
try:
|
|
1752
|
+
from compass.providers import get_adapter
|
|
1753
|
+
adapter = get_adapter("vectorstore", settings.redis.provider, config=settings.redis)
|
|
1754
|
+
if not getattr(adapter, "ping", lambda: True)():
|
|
1755
|
+
adapter = None
|
|
1756
|
+
except Exception: # noqa: BLE001 - degrade to Level 1
|
|
1757
|
+
adapter = None
|
|
1758
|
+
|
|
1759
|
+
print(f" Enriching {len(cases)} case(s) with deterministic KB signals "
|
|
1760
|
+
f"(level {'1+2' if adapter else '1'}) ...", file=sys.stderr)
|
|
1761
|
+
enriched, summary = enrich_cases(cases, settings.eval, adapter)
|
|
1762
|
+
if isinstance(data, dict):
|
|
1763
|
+
data["compass_summary"] = summary
|
|
1764
|
+
out_obj = data
|
|
1765
|
+
else:
|
|
1766
|
+
out_obj = enriched # a list: each case now carries a "compass" block; shape preserved
|
|
1767
|
+
body = _json.dumps(out_obj, indent=2, ensure_ascii=False)
|
|
1768
|
+
mean = summary["mean_citation_grounding_rate"]
|
|
1769
|
+
note = (f"{summary['cases_enriched']} enriched, {summary['cases_with_verifiable_claims']} "
|
|
1770
|
+
f"with verifiable claims" + (f", mean grounding rate {mean:.0%}" if mean is not None else ""))
|
|
1771
|
+
if args.output:
|
|
1772
|
+
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
|
1773
|
+
Path(args.output).write_text(body + "\n", encoding="utf-8")
|
|
1774
|
+
print(f" Wrote {args.output} ({note})", file=sys.stderr)
|
|
1775
|
+
else:
|
|
1776
|
+
print(body)
|
|
1777
|
+
print(f" {note}", file=sys.stderr)
|
|
1778
|
+
return 0
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _format_calibration(report: dict, metric: str) -> str:
|
|
1782
|
+
lines = [f"Judge calibration for '{metric}' vs COMPASS deterministic truth "
|
|
1783
|
+
f"({report['cases_scored']} case(s), tolerance +/-{report['tolerance']}):", ""]
|
|
1784
|
+
rows = sorted(report["judges"].items(),
|
|
1785
|
+
key=lambda kv: (kv[1]["avg_error"] is None, kv[1]["avg_error"] or 0))
|
|
1786
|
+
lines.append(f" {'judge':<18} {'n':>4} {'error':>8} {'bias':>8} {'acc':>7} {'weight':>8}")
|
|
1787
|
+
for name, v in rows:
|
|
1788
|
+
lean = "" if v["bias"] is None else (" (lenient)" if v["bias"] > 0.03 else
|
|
1789
|
+
" (strict)" if v["bias"] < -0.03 else "")
|
|
1790
|
+
lines.append(f" {name:<18} {v['n']:>4} {v['avg_error']:>8.3f} {v['bias']:>+8.3f} "
|
|
1791
|
+
f"{v['accuracy']:>6.0%} {v['recommended_weight']:>8.3f}{lean}")
|
|
1792
|
+
if report.get("most_accurate"):
|
|
1793
|
+
lines += ["", f" Most accurate: {report['most_accurate']}. Recommended weights are "
|
|
1794
|
+
"inverse-error (a more accurate judge is trusted more than equal weighting)."]
|
|
1795
|
+
lines += ["", " " + report["note"]]
|
|
1796
|
+
return "\n".join(lines)
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
def cmd_calibrate_judges(argv: list[str]) -> int:
|
|
1800
|
+
p = argparse.ArgumentParser(
|
|
1801
|
+
prog="compass calibrate-judges",
|
|
1802
|
+
description="Measure how accurate each LLM judge is against COMPASS's deterministic "
|
|
1803
|
+
"grounding truth: per-judge error, bias, accuracy, and an inverse-error "
|
|
1804
|
+
"recommended weight. Input cases must carry both COMPASS truth (run "
|
|
1805
|
+
"'compass enrich' first) and the eval's per-judge scores. Schema-agnostic.")
|
|
1806
|
+
p.add_argument("--input", required=True, metavar="FILE")
|
|
1807
|
+
p.add_argument("--metric", required=True,
|
|
1808
|
+
help="Judge metric to calibrate (e.g. 'groundedness'); matched case-insensitively")
|
|
1809
|
+
p.add_argument("--truth-path", default="compass.grounding.citation_grounding_rate",
|
|
1810
|
+
help="Dotted path to COMPASS truth per case (default: the 'enrich' output field)")
|
|
1811
|
+
p.add_argument("--scores-path", default="",
|
|
1812
|
+
help="Dotted path to the per-case scores block (default: auto-discover)")
|
|
1813
|
+
p.add_argument("--tolerance", type=float, default=0.2,
|
|
1814
|
+
help="A judge score within this of the truth counts as accurate (default 0.2)")
|
|
1815
|
+
p.add_argument("--json", action="store_true")
|
|
1816
|
+
args = p.parse_args(argv)
|
|
1817
|
+
|
|
1818
|
+
import json as _json
|
|
1819
|
+
|
|
1820
|
+
from compass.eval_enrich import calibrate_judges, discover_cases, extract_judge_scores
|
|
1821
|
+
from compass.eval_interop import get_path
|
|
1822
|
+
|
|
1823
|
+
try:
|
|
1824
|
+
with open(args.input, encoding="utf-8") as f:
|
|
1825
|
+
data = _json.load(f)
|
|
1826
|
+
except (OSError, ValueError) as exc:
|
|
1827
|
+
print(f" Cannot read {args.input}: {exc}", file=sys.stderr)
|
|
1828
|
+
return 2
|
|
1829
|
+
cases = discover_cases(data)
|
|
1830
|
+
if not cases:
|
|
1831
|
+
print(f" No case array found in {args.input}.", file=sys.stderr)
|
|
1832
|
+
return 2
|
|
1833
|
+
|
|
1834
|
+
pairs, truth_found, judges_found = [], 0, 0
|
|
1835
|
+
for c in cases:
|
|
1836
|
+
truth = get_path(c, args.truth_path)
|
|
1837
|
+
judges = extract_judge_scores(c, args.metric, args.scores_path or None)
|
|
1838
|
+
if isinstance(truth, (int, float)) and not isinstance(truth, bool):
|
|
1839
|
+
truth_found += 1
|
|
1840
|
+
if judges:
|
|
1841
|
+
judges_found += 1
|
|
1842
|
+
if isinstance(truth, (int, float)) and not isinstance(truth, bool) and judges:
|
|
1843
|
+
pairs.append({"truth": float(truth), "judges": judges})
|
|
1844
|
+
if not pairs:
|
|
1845
|
+
print(f" No comparable cases. COMPASS truth found in {truth_found}/{len(cases)} case(s) "
|
|
1846
|
+
f"(path '{args.truth_path}'); judge scores for metric '{args.metric}' in "
|
|
1847
|
+
f"{judges_found}/{len(cases)}. Run 'compass enrich' first, and check --metric, "
|
|
1848
|
+
f"--scores-path, and --truth-path.", file=sys.stderr)
|
|
1849
|
+
return 2
|
|
1850
|
+
report = calibrate_judges(pairs, tolerance=args.tolerance)
|
|
1851
|
+
print(_json.dumps(report, indent=2, ensure_ascii=False) if args.json
|
|
1852
|
+
else _format_calibration(report, args.metric))
|
|
1853
|
+
return 0
|
|
1854
|
+
|
|
1855
|
+
|
|
1704
1856
|
_HELP = """compass - knowledge-base readiness validation
|
|
1705
1857
|
|
|
1706
1858
|
Usage: compass <command> [options]
|
|
@@ -1716,6 +1868,8 @@ Commands:
|
|
|
1716
1868
|
freshness flag stale sources + orphan documents (source object gone)
|
|
1717
1869
|
evidence produce live, reproducible PROOF of flagged issues (chunks + queries) - no LLM
|
|
1718
1870
|
grounding-audit check an eval's grounding finding against the KB (count claims) - no LLM
|
|
1871
|
+
enrich add deterministic KB signals (grounding rate, chunk quality) to an eval file
|
|
1872
|
+
calibrate-judges measure each LLM judge's accuracy/bias vs COMPASS's deterministic truth
|
|
1719
1873
|
demo generate a sample report from synthetic data (no backend needed)
|
|
1720
1874
|
init [name] scaffold a project (config + .env.example + marker). -i/--interactive wizard; --graph graph block; --ci workflow
|
|
1721
1875
|
lint statically check a project config for silent-failure traps (no backend)
|
|
@@ -1764,6 +1918,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1764
1918
|
return cmd_evidence(rest)
|
|
1765
1919
|
if cmd == "grounding-audit":
|
|
1766
1920
|
return cmd_grounding_audit(rest)
|
|
1921
|
+
if cmd == "enrich":
|
|
1922
|
+
return cmd_enrich(rest)
|
|
1923
|
+
if cmd == "calibrate-judges":
|
|
1924
|
+
return cmd_calibrate_judges(rest)
|
|
1767
1925
|
if cmd == "freshness":
|
|
1768
1926
|
return cmd_freshness(rest)
|
|
1769
1927
|
if cmd == "selftest":
|