compass-kb-validation 3.2.1__tar.gz → 3.3.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.3.0}/PKG-INFO +15 -1
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/README.md +14 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/__init__.py +1 -1
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/cli.py +158 -0
- compass_kb_validation-3.3.0/compass/eval_enrich.py +305 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/grounding.py +5 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/PKG-INFO +15 -1
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/SOURCES.txt +2 -0
- compass_kb_validation-3.3.0/tests/test_eval_enrich.py +109 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/LICENSE +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/__main__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/ask.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/baseline.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/chunk_coherence.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/citation.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/config/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/config_lint.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/content_inspect.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/corpus.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/corpus_health.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/diagnostics.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/embedding_drift.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/eval_interop.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/evidence.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/init_wizard.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/live_dashboard.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/mcp_server.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/models/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/notifications.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/pipeline/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/profiler.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/provenance.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/providers/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/providers/embedding.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/providers/graph.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/providers/source.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/providers/storage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/providers/vectorstore.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/remediation.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/report/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/report/_report_css.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/report/_report_js.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/report/analysis.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/report/audit.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/report/export.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/report/shell.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/retrieval_analysis.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/text_probes.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/trend_tracker.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/utils/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/api_upload.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/base.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/base_sparql.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/cross_stage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/graphdb_metadata.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/neptune_metadata.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/redis_chunks.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/registry.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/retrieval_quality.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/s3_storage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/requires.txt +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/discover_and_validate.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/ingestion_validation/__init__.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/profile_kb.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/pyproject.toml +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/run_validation.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/setup.cfg +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_alt_adapters.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_analysis_report.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_api_upload.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_ask.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_baseline.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_benchmark.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_check_catalog.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_citation.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_cli.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_compat_shim.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_config.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_config_lint.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_content_inspect.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_corpus.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_corpus_health.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_coverage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_cross_stage.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_diagnostics.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_discover_cli.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_discovery_parallel.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_embedding_drift.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_eval_interop.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_evidence.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_gate.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_graph_transport.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_graph_validators.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_grounding.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_init_wizard.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_manifest.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_mcp.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_migration.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_models.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_neptune_bugfixes.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_neptune_bugfixes2.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_notifications.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_pipeline.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_profiler.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_providers.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_redis_chunks.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_registry.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_remediation.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_reporting.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_retrieval_assertions.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_retrieval_quality.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_review_hardening.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_run_cli.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_sources.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_storage_validator.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_trends.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_universal_concerns.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_utils.py +0 -0
- {compass_kb_validation-3.2.1 → compass_kb_validation-3.3.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.3.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.
|
|
@@ -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":
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Deterministic eval enrichment + judge calibration - the "ground-truth oracle".
|
|
2
|
+
|
|
3
|
+
COMPASS emits deterministic, KB-derived signals for eval cases, and measures how accurate an
|
|
4
|
+
eval's own (LLM-judge) scores are against that deterministic truth. This is the read-only query
|
|
5
|
+
surface applied to an eval pipeline: it imports nothing from the validation pipeline, never calls
|
|
6
|
+
an LLM, and never changes a validation pass/fail - additive, exactly like ``ask`` / ``compare``.
|
|
7
|
+
|
|
8
|
+
HONEST SCOPE. A fully deterministic groundedness score over *every* claim is not possible without
|
|
9
|
+
an LLM (general prose-claim extraction needs one). COMPASS scores the VERIFIABLE SUBSET only -
|
|
10
|
+
citation-bearing and numeric/aggregation claims - which happens to be exactly where LLM judges
|
|
11
|
+
disagree most, so it is the sharp edge, not a limitation to hide. The signal is therefore named a
|
|
12
|
+
*citation grounding rate*, never "Groundedness", so nothing implies full-claim LLM parity.
|
|
13
|
+
|
|
14
|
+
Two capabilities:
|
|
15
|
+
* ``enrich_cases`` - per-case deterministic signals (citation grounding rate + fidelity, chunk
|
|
16
|
+
quality, context utilization) plus COMPASS_* signal codes.
|
|
17
|
+
* ``calibrate_judges`` - given each case's deterministic truth and the eval's per-judge scores,
|
|
18
|
+
report each judge's error, bias, accuracy, and an evidence-based recommended weight. This is
|
|
19
|
+
the part no eval framework can produce without KB access.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
from compass.eval_interop import get_path
|
|
27
|
+
|
|
28
|
+
# Deterministic signal codes COMPASS owns. KB-derived only - no answer-quality / guardrail / SQL.
|
|
29
|
+
GROUNDED, PARTIAL, UNGROUNDED = "COMPASS_GROUNDED", "COMPASS_PARTIAL_GROUND", "COMPASS_UNGROUNDED"
|
|
30
|
+
_FAULT_CODES = {
|
|
31
|
+
"fabricated_source": "COMPASS_FABRICATION",
|
|
32
|
+
"retrieval": "COMPASS_RETRIEVAL_GAP",
|
|
33
|
+
"generation": "COMPASS_GENERATION_HALLUCINATION",
|
|
34
|
+
"citation_error": "COMPASS_CITATION_ERROR",
|
|
35
|
+
}
|
|
36
|
+
# Thresholds (deterministic, overridable by the caller).
|
|
37
|
+
DEFAULTS = {
|
|
38
|
+
"grounded_at": 0.8, # citation grounding rate at/above this -> GROUNDED
|
|
39
|
+
"partial_at": 0.5, # between partial_at and grounded_at -> PARTIAL_GROUND
|
|
40
|
+
"context_waste_below_pct": 20.0,
|
|
41
|
+
"chunk_quality_floor": 0.5,
|
|
42
|
+
"uncited_numeric_max": 3,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _citation_grounding_rate(summary: dict) -> Optional[float]:
|
|
47
|
+
"""Sound / checkable over the VERIFIABLE subset. None when nothing was checkable (so it is
|
|
48
|
+
never confused with a real 0.0 - "no verifiable claims" is not "ungrounded")."""
|
|
49
|
+
checkable = summary.get("checkable", 0) or 0
|
|
50
|
+
if not checkable:
|
|
51
|
+
return None
|
|
52
|
+
return round((summary.get("sound", 0) or 0) / checkable, 4)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _case_chunk_quality(record: dict, cfg: Any, min_chars: int) -> Optional[dict]:
|
|
56
|
+
"""Score the chunks THIS case was given, deterministically. None if no chunks are present."""
|
|
57
|
+
from compass.corpus_health import _chunk_quality
|
|
58
|
+
|
|
59
|
+
chunks = get_path(record, getattr(cfg, "gt_chunks_path", "context.chunks"))
|
|
60
|
+
if not isinstance(chunks, list) or not chunks:
|
|
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:
|
|
67
|
+
return None
|
|
68
|
+
text_field = getattr(cfg, "gt_chunk_text_field", "text") or "text"
|
|
69
|
+
scores, not_self_contained = [], 0
|
|
70
|
+
for c in chunks:
|
|
71
|
+
text = (c.get(text_field) or c.get("text") or "") if isinstance(c, dict) else ""
|
|
72
|
+
q = _chunk_quality(text, min_chars, boilerplate_lines=set())
|
|
73
|
+
scores.append(q["score"])
|
|
74
|
+
if not q["self_contained"]:
|
|
75
|
+
not_self_contained += 1
|
|
76
|
+
if not scores:
|
|
77
|
+
return None
|
|
78
|
+
return {
|
|
79
|
+
"mean_score": round(sum(scores) / len(scores), 3),
|
|
80
|
+
"min_score": round(min(scores), 3),
|
|
81
|
+
"not_self_contained_pct": round(100.0 * not_self_contained / len(scores), 1),
|
|
82
|
+
"chunks_scored": len(scores),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _derive_signals(grounding: dict, chunk_quality: Optional[dict], thr: dict) -> list[dict]:
|
|
87
|
+
"""Turn the deterministic measurements into COMPASS_* signal codes. Every code is
|
|
88
|
+
check-backed; there is no heuristic guessing here."""
|
|
89
|
+
signals: list[dict] = []
|
|
90
|
+
rate = grounding.get("citation_grounding_rate")
|
|
91
|
+
if rate is not None:
|
|
92
|
+
if rate >= thr["grounded_at"]:
|
|
93
|
+
code, sev = GROUNDED, "INFO"
|
|
94
|
+
elif rate >= thr["partial_at"]:
|
|
95
|
+
code, sev = PARTIAL, "MEDIUM"
|
|
96
|
+
else:
|
|
97
|
+
code, sev = UNGROUNDED, "HIGH"
|
|
98
|
+
signals.append({"code": code, "severity": sev,
|
|
99
|
+
"detail": f"citation grounding rate {rate:.0%} over "
|
|
100
|
+
f"{grounding.get('checkable', 0)} verifiable claim(s)"})
|
|
101
|
+
for fault, n in (grounding.get("faults") or {}).items():
|
|
102
|
+
code = _FAULT_CODES.get(fault)
|
|
103
|
+
if code and n:
|
|
104
|
+
signals.append({"code": code, "severity": "HIGH", "detail": f"{n} {fault} fault(s)"})
|
|
105
|
+
util = grounding.get("context_utilization_pct")
|
|
106
|
+
if util is not None and util < thr["context_waste_below_pct"]:
|
|
107
|
+
signals.append({"code": "COMPASS_CONTEXT_WASTE", "severity": "LOW",
|
|
108
|
+
"detail": f"only {util:.0f}% of retrieved chunks were cited"})
|
|
109
|
+
if chunk_quality and chunk_quality["min_score"] < thr["chunk_quality_floor"]:
|
|
110
|
+
signals.append({"code": "COMPASS_CHUNK_QUALITY", "severity": "LOW",
|
|
111
|
+
"detail": f"worst chunk score {chunk_quality['min_score']:.2f}, "
|
|
112
|
+
f"{chunk_quality['not_self_contained_pct']:.0f}% not self-contained"})
|
|
113
|
+
if (grounding.get("uncited_numeric_claims") or 0) > thr["uncited_numeric_max"]:
|
|
114
|
+
signals.append({"code": "COMPASS_UNCITED_NUMERIC", "severity": "MEDIUM",
|
|
115
|
+
"detail": f"{grounding['uncited_numeric_claims']} numeric claim(s) carry no citation"})
|
|
116
|
+
return signals
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def enrich_case(record: dict, cfg: Any, adapter: Any = None, *,
|
|
120
|
+
min_chars: int = 50, thresholds: Optional[dict] = None) -> dict:
|
|
121
|
+
"""Deterministic ``compass`` block for one eval record.
|
|
122
|
+
|
|
123
|
+
``adapter`` optional: with a reachable KB the citation audit also runs Level 2 (KB-verified);
|
|
124
|
+
without it, Level 1 (against the case's own chunks) still produces a grounding rate. No LLM.
|
|
125
|
+
"""
|
|
126
|
+
from compass.grounding import run_audit
|
|
127
|
+
|
|
128
|
+
thr = {**DEFAULTS, **(thresholds or {})}
|
|
129
|
+
res = run_audit(record, adapter, cfg)
|
|
130
|
+
summary = res.citations.get("summary", {}) if isinstance(res.citations, dict) else {}
|
|
131
|
+
grounding = {
|
|
132
|
+
"citation_grounding_rate": _citation_grounding_rate(summary),
|
|
133
|
+
"total_citations": summary.get("total_citations", 0),
|
|
134
|
+
"checkable": summary.get("checkable", 0),
|
|
135
|
+
"sound": summary.get("sound", 0),
|
|
136
|
+
"faults": summary.get("faults", {}),
|
|
137
|
+
"context_utilization_pct": summary.get("context_utilization_pct"),
|
|
138
|
+
"uncited_numeric_claims": summary.get("uncited_numeric_claims", 0),
|
|
139
|
+
"context_empty": res.context_empty,
|
|
140
|
+
"levels": summary.get("levels", "none"),
|
|
141
|
+
}
|
|
142
|
+
if summary.get("diagnostic"):
|
|
143
|
+
grounding["diagnostic"] = summary["diagnostic"]
|
|
144
|
+
chunk_quality = _case_chunk_quality(record, cfg, min_chars)
|
|
145
|
+
block: dict = {"grounding": grounding, "method": "compass_deterministic"}
|
|
146
|
+
if chunk_quality:
|
|
147
|
+
block["chunk_quality"] = chunk_quality
|
|
148
|
+
block["signals"] = _derive_signals(grounding, chunk_quality, thr)
|
|
149
|
+
return block
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def enrich_cases(records: list[dict], cfg: Any, adapter: Any = None, *,
|
|
153
|
+
min_chars: int = 50, thresholds: Optional[dict] = None) -> tuple[list[dict], dict]:
|
|
154
|
+
"""Enrich every record (adds ``record["compass"]``) and return (records, aggregate summary)."""
|
|
155
|
+
rates: list[float] = []
|
|
156
|
+
code_counts: dict[str, int] = {}
|
|
157
|
+
enriched = 0
|
|
158
|
+
for rec in records:
|
|
159
|
+
if not isinstance(rec, dict):
|
|
160
|
+
continue
|
|
161
|
+
block = enrich_case(rec, cfg, adapter, min_chars=min_chars, thresholds=thresholds)
|
|
162
|
+
rec["compass"] = block
|
|
163
|
+
enriched += 1
|
|
164
|
+
r = block["grounding"].get("citation_grounding_rate")
|
|
165
|
+
if r is not None:
|
|
166
|
+
rates.append(r)
|
|
167
|
+
for s in block["signals"]:
|
|
168
|
+
code_counts[s["code"]] = code_counts.get(s["code"], 0) + 1
|
|
169
|
+
summary = {
|
|
170
|
+
"cases_enriched": enriched,
|
|
171
|
+
"cases_with_verifiable_claims": len(rates),
|
|
172
|
+
"mean_citation_grounding_rate": round(sum(rates) / len(rates), 4) if rates else None,
|
|
173
|
+
"signal_breakdown": dict(sorted(code_counts.items(), key=lambda kv: (-kv[1], kv[0]))),
|
|
174
|
+
"note": "Deterministic KB-derived signals over the verifiable (citation + numeric) subset. "
|
|
175
|
+
"Not an answer-quality judgement.",
|
|
176
|
+
}
|
|
177
|
+
return records, summary
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def discover_cases(data: Any) -> list[dict]:
|
|
181
|
+
"""Find the array of case objects in an arbitrary eval file - COMPASS assumes no fixed shape.
|
|
182
|
+
|
|
183
|
+
Accepts a top-level list, or a dict carrying the cases under a common key
|
|
184
|
+
(cases/results/records/evaluations/data/items), or - failing that - the largest list of
|
|
185
|
+
objects anywhere at the top level. Returns [] if none is found.
|
|
186
|
+
"""
|
|
187
|
+
if isinstance(data, list):
|
|
188
|
+
return [c for c in data if isinstance(c, dict)]
|
|
189
|
+
if isinstance(data, dict):
|
|
190
|
+
for key in ("cases", "results", "records", "evaluations", "data", "items"):
|
|
191
|
+
v = data.get(key)
|
|
192
|
+
if isinstance(v, list) and v and isinstance(v[0], dict):
|
|
193
|
+
return v
|
|
194
|
+
best: list[dict] = []
|
|
195
|
+
for v in data.values():
|
|
196
|
+
if isinstance(v, list) and v and isinstance(v[0], dict) and len(v) > len(best):
|
|
197
|
+
best = v
|
|
198
|
+
return best
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _numbers_from(obj: Any) -> dict[str, float]:
|
|
203
|
+
"""Pull judge_name -> score out of whatever sub-shape a metric block uses: a dict of
|
|
204
|
+
name->number, a dict with a 'judges'/'scores' sub-map, or a list of {judge/model/name, score}."""
|
|
205
|
+
out: dict[str, float] = {}
|
|
206
|
+
if isinstance(obj, dict):
|
|
207
|
+
inner = obj.get("judges") or obj.get("scores") or obj.get("raters")
|
|
208
|
+
if isinstance(inner, (dict, list)):
|
|
209
|
+
return _numbers_from(inner)
|
|
210
|
+
for k, v in obj.items():
|
|
211
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool) and k.lower() not in (
|
|
212
|
+
"score", "spread", "mean", "avg", "average", "std", "count", "n"):
|
|
213
|
+
out[str(k)] = float(v)
|
|
214
|
+
elif isinstance(obj, list):
|
|
215
|
+
for i, item in enumerate(obj):
|
|
216
|
+
if isinstance(item, dict):
|
|
217
|
+
name = str(item.get("judge") or item.get("model") or item.get("name") or f"judge_{i}")
|
|
218
|
+
score = item.get("score")
|
|
219
|
+
if isinstance(score, (int, float)) and not isinstance(score, bool):
|
|
220
|
+
out[name] = float(score)
|
|
221
|
+
return out
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def extract_judge_scores(case: dict, metric: str, scores_path: Optional[str] = None) -> dict[str, float]:
|
|
225
|
+
"""Best-effort, schema-agnostic extraction of per-judge scores for ``metric`` from one case.
|
|
226
|
+
|
|
227
|
+
``scores_path`` (optional) points at the scores block; otherwise common locations
|
|
228
|
+
(scores/metrics/evaluations) are tried. The metric is matched case-insensitively (spaces and
|
|
229
|
+
underscores treated alike). Returns {judge_name: score}; empty if nothing matched.
|
|
230
|
+
"""
|
|
231
|
+
blocks = []
|
|
232
|
+
if scores_path:
|
|
233
|
+
blocks.append(get_path(case, scores_path))
|
|
234
|
+
else:
|
|
235
|
+
for key in ("scores", "metrics", "evaluations", "judges"):
|
|
236
|
+
if key in case:
|
|
237
|
+
blocks.append(case.get(key))
|
|
238
|
+
want = metric.lower().replace(" ", "_")
|
|
239
|
+
for block in blocks:
|
|
240
|
+
if not isinstance(block, dict):
|
|
241
|
+
continue
|
|
242
|
+
for name, detail in block.items():
|
|
243
|
+
if name.lower().replace(" ", "_") == want:
|
|
244
|
+
nums = _numbers_from(detail)
|
|
245
|
+
if nums:
|
|
246
|
+
return nums
|
|
247
|
+
return {}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def calibrate_judges(cases: list[dict], *, tolerance: float = 0.2) -> dict:
|
|
251
|
+
"""Measure each LLM judge against COMPASS's deterministic truth - the oracle capability.
|
|
252
|
+
|
|
253
|
+
``cases``: ``[{"truth": float 0..1, "judges": {judge_name: score 0..1}}]`` where ``truth`` is
|
|
254
|
+
COMPASS's deterministic grounding rate for that case. For each judge, over the cases where both
|
|
255
|
+
a truth and that judge's score exist, report:
|
|
256
|
+
* ``avg_error`` - mean absolute error vs the deterministic truth
|
|
257
|
+
* ``bias`` - mean signed error (+ = lenient / scores high, - = strict / scores low)
|
|
258
|
+
* ``accuracy`` - share of scores within ``tolerance`` of the truth
|
|
259
|
+
* ``recommended_weight`` - inverse-error weight (normalised across judges), so a more
|
|
260
|
+
accurate judge is trusted more than an equal-weight consensus would.
|
|
261
|
+
Deterministic and reproducible. No eval framework can compute this - it needs the KB truth.
|
|
262
|
+
"""
|
|
263
|
+
stats: dict[str, dict] = {}
|
|
264
|
+
scored = 0
|
|
265
|
+
for c in cases:
|
|
266
|
+
truth = c.get("truth")
|
|
267
|
+
judges = c.get("judges") or {}
|
|
268
|
+
if not isinstance(truth, (int, float)) or not judges:
|
|
269
|
+
continue
|
|
270
|
+
scored += 1
|
|
271
|
+
for name, score in judges.items():
|
|
272
|
+
if not isinstance(score, (int, float)):
|
|
273
|
+
continue
|
|
274
|
+
s = stats.setdefault(name, {"abs": 0.0, "signed": 0.0, "hits": 0, "n": 0})
|
|
275
|
+
err = score - truth
|
|
276
|
+
s["abs"] += abs(err)
|
|
277
|
+
s["signed"] += err
|
|
278
|
+
s["hits"] += 1 if abs(err) <= tolerance else 0
|
|
279
|
+
s["n"] += 1
|
|
280
|
+
|
|
281
|
+
judges_out: dict[str, dict] = {}
|
|
282
|
+
for name, s in stats.items():
|
|
283
|
+
n = s["n"]
|
|
284
|
+
judges_out[name] = {
|
|
285
|
+
"n": n,
|
|
286
|
+
"avg_error": round(s["abs"] / n, 4) if n else None,
|
|
287
|
+
"bias": round(s["signed"] / n, 4) if n else None,
|
|
288
|
+
"accuracy": round(s["hits"] / n, 4) if n else None,
|
|
289
|
+
}
|
|
290
|
+
# Recommended weights: inverse average error, normalised. A judge with no scored cases gets 0.
|
|
291
|
+
inv = {name: (1.0 / (v["avg_error"] + 1e-6)) if v["avg_error"] is not None else 0.0
|
|
292
|
+
for name, v in judges_out.items()}
|
|
293
|
+
total_inv = sum(inv.values())
|
|
294
|
+
for name, v in judges_out.items():
|
|
295
|
+
v["recommended_weight"] = round(inv[name] / total_inv, 4) if total_inv else None
|
|
296
|
+
ranked = sorted(judges_out, key=lambda nm: (judges_out[nm]["avg_error"] is None,
|
|
297
|
+
judges_out[nm]["avg_error"] if judges_out[nm]["avg_error"] is not None else 1e9))
|
|
298
|
+
return {
|
|
299
|
+
"cases_scored": scored,
|
|
300
|
+
"tolerance": tolerance,
|
|
301
|
+
"judges": judges_out,
|
|
302
|
+
"most_accurate": ranked[0] if ranked else None,
|
|
303
|
+
"note": "Judge scores measured against COMPASS's deterministic grounding truth "
|
|
304
|
+
"(citation + numeric subset). Recommended weights are inverse-error, normalised.",
|
|
305
|
+
}
|
|
@@ -422,6 +422,11 @@ def run_audit(eval_record: dict, adapter: Any, eval_cfg: Any, doc_exists: Any =
|
|
|
422
422
|
get_path(eval_record, "chunks"))
|
|
423
423
|
has_chunks = any(isinstance(c, list) and len(c) > 0 for c in _chunk_candidates)
|
|
424
424
|
context_empty = _is_empty_context(context) and not has_chunks
|
|
425
|
+
# If the configured chunk path did not resolve but a common nested one did, use it for the
|
|
426
|
+
# citation audit too - otherwise a default-path mismatch silently disables Level 1
|
|
427
|
+
# verification (the same class of schema mismatch as the empty-retrieval false positive).
|
|
428
|
+
if not (isinstance(eval_chunks, list) and eval_chunks):
|
|
429
|
+
eval_chunks = next((c for c in _chunk_candidates if isinstance(c, list) and c), eval_chunks)
|
|
425
430
|
|
|
426
431
|
qtype, intents = classify_query(query, getattr(eval_cfg, "structured_query_patterns", []))
|
|
427
432
|
result = GroundingResult(test_id=test_id, query=query, query_type=qtype, intents=intents,
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass_kb_validation.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compass-kb-validation
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.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.
|
|
@@ -17,6 +17,7 @@ compass/corpus.py
|
|
|
17
17
|
compass/corpus_health.py
|
|
18
18
|
compass/diagnostics.py
|
|
19
19
|
compass/embedding_drift.py
|
|
20
|
+
compass/eval_enrich.py
|
|
20
21
|
compass/eval_interop.py
|
|
21
22
|
compass/evidence.py
|
|
22
23
|
compass/grounding.py
|
|
@@ -86,6 +87,7 @@ tests/test_diagnostics.py
|
|
|
86
87
|
tests/test_discover_cli.py
|
|
87
88
|
tests/test_discovery_parallel.py
|
|
88
89
|
tests/test_embedding_drift.py
|
|
90
|
+
tests/test_eval_enrich.py
|
|
89
91
|
tests/test_eval_interop.py
|
|
90
92
|
tests/test_evidence.py
|
|
91
93
|
tests/test_gate.py
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Deterministic eval enrichment + judge calibration (the ground-truth oracle).
|
|
2
|
+
|
|
3
|
+
All synthetic and generic - no vendor/domain data, no live services. The point is the
|
|
4
|
+
deterministic scoring math and the schema-agnostic field/shape discovery.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from compass.config import Settings
|
|
10
|
+
from compass.eval_enrich import (
|
|
11
|
+
calibrate_judges,
|
|
12
|
+
discover_cases,
|
|
13
|
+
enrich_cases,
|
|
14
|
+
extract_judge_scores,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _case(output, chunk_text, doc="Doc A", page=1):
|
|
19
|
+
return {"input": "q", "output": output,
|
|
20
|
+
"context": {"chunks": [{"text": chunk_text, "document_title": doc, "page_start": page}]}}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_enrich_scores_a_grounded_numeric_claim():
|
|
24
|
+
cfg = Settings().eval
|
|
25
|
+
cases = [_case("Revenue rose 8%. (Doc A, 2026, p. 1) End.", "Revenue rose 8% year over year.")]
|
|
26
|
+
enriched, summary = enrich_cases(cases, cfg) # no adapter -> Level 1, no backend
|
|
27
|
+
g = enriched[0]["compass"]["grounding"]
|
|
28
|
+
assert g["citation_grounding_rate"] == 1.0 and g["checkable"] == 1 and g["levels"] == "L1"
|
|
29
|
+
assert "COMPASS_GROUNDED" in [s["code"] for s in enriched[0]["compass"]["signals"]]
|
|
30
|
+
assert summary["mean_citation_grounding_rate"] == 1.0
|
|
31
|
+
assert enriched[0]["compass"]["method"] == "compass_deterministic"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_grounding_rate_is_none_when_nothing_is_verifiable():
|
|
35
|
+
# Honest subset: a purely qualitative cited claim has nothing deterministically checkable,
|
|
36
|
+
# so the rate is None (NOT a real 0.0 - "no verifiable claims" is not "ungrounded").
|
|
37
|
+
cfg = Settings().eval
|
|
38
|
+
cases = [_case("Growth stays robust. (Doc A, 2026, p. 1) Good.", "Growth stays robust next year.")]
|
|
39
|
+
enriched, _ = enrich_cases(cases, cfg)
|
|
40
|
+
g = enriched[0]["compass"]["grounding"]
|
|
41
|
+
assert g["citation_grounding_rate"] is None and g["checkable"] == 0
|
|
42
|
+
assert g["total_citations"] == 1 # the citation was still extracted
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_enrich_is_schema_agnostic_via_config_paths():
|
|
46
|
+
# A completely different eval shape resolves purely from eval.gt_* config - nothing hardcoded.
|
|
47
|
+
cfg = Settings().eval
|
|
48
|
+
cfg.gt_response_path = "answer"
|
|
49
|
+
cfg.gt_chunks_path = "retrieval.docs"
|
|
50
|
+
cfg.gt_chunk_text_field = "chunk_text"
|
|
51
|
+
cfg.gt_chunk_doc_field = "src"
|
|
52
|
+
cfg.gt_chunk_page_field = "pg"
|
|
53
|
+
rec = {"query": "q", "answer": "Margins hit 21%. (D, 2026, p. 2) End.",
|
|
54
|
+
"retrieval": {"docs": [{"chunk_text": "Margins hit 21% in the period.", "src": "D", "pg": 2}]}}
|
|
55
|
+
enriched, _ = enrich_cases([rec], cfg)
|
|
56
|
+
assert enriched[0]["compass"]["grounding"]["citation_grounding_rate"] == 1.0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_calibrate_judges_ranks_by_error_and_weights_inversely():
|
|
60
|
+
# judge_a is close to the deterministic truth; judge_b is systematically low. The more
|
|
61
|
+
# accurate judge must rank first and earn the larger recommended weight.
|
|
62
|
+
cases = [
|
|
63
|
+
{"truth": 0.9, "judges": {"judge_a": 0.85, "judge_b": 0.55}},
|
|
64
|
+
{"truth": 0.8, "judges": {"judge_a": 0.82, "judge_b": 0.50}},
|
|
65
|
+
{"truth": 1.0, "judges": {"judge_a": 0.95, "judge_b": 0.60}},
|
|
66
|
+
]
|
|
67
|
+
r = calibrate_judges(cases, tolerance=0.2)
|
|
68
|
+
assert r["cases_scored"] == 3 and r["most_accurate"] == "judge_a"
|
|
69
|
+
a, b = r["judges"]["judge_a"], r["judges"]["judge_b"]
|
|
70
|
+
assert a["avg_error"] < b["avg_error"]
|
|
71
|
+
assert a["recommended_weight"] > b["recommended_weight"]
|
|
72
|
+
assert a["accuracy"] == 1.0 and b["accuracy"] == 0.0
|
|
73
|
+
assert b["bias"] < 0 # judge_b scores low (strict)
|
|
74
|
+
# weights are a normalised distribution
|
|
75
|
+
assert abs(a["recommended_weight"] + b["recommended_weight"] - 1.0) < 1e-6
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_calibrate_ignores_cases_missing_truth_or_judges():
|
|
79
|
+
cases = [
|
|
80
|
+
{"truth": 0.9, "judges": {"j": 0.9}},
|
|
81
|
+
{"truth": None, "judges": {"j": 0.5}}, # no truth -> skipped
|
|
82
|
+
{"judges": {"j": 0.5}}, # no truth key -> skipped
|
|
83
|
+
{"truth": 0.8}, # no judges -> skipped
|
|
84
|
+
]
|
|
85
|
+
r = calibrate_judges(cases)
|
|
86
|
+
assert r["cases_scored"] == 1 and r["judges"]["j"]["n"] == 1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_discover_cases_finds_the_array_in_varied_shapes():
|
|
90
|
+
assert len(discover_cases([{"a": 1}, {"a": 2}])) == 2 # top-level list
|
|
91
|
+
assert len(discover_cases({"cases": [{"a": 1}]})) == 1 # common key
|
|
92
|
+
assert len(discover_cases({"results": [{"a": 1}, {"a": 2}]})) == 2
|
|
93
|
+
# largest list of objects wins when no known key
|
|
94
|
+
assert len(discover_cases({"meta": {}, "rows": [{"a": 1}, {"a": 2}, {"a": 3}]})) == 3
|
|
95
|
+
assert discover_cases({"x": 1}) == []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_extract_judge_scores_handles_varied_shapes():
|
|
99
|
+
# dict of name->number nested under 'judges'
|
|
100
|
+
c1 = {"scores": {"Groundedness": {"judges": {"j1": 0.8, "j2": 0.6}}}}
|
|
101
|
+
assert extract_judge_scores(c1, "groundedness") == {"j1": 0.8, "j2": 0.6}
|
|
102
|
+
# list of {judge, score}
|
|
103
|
+
c2 = {"metrics": {"groundedness": [{"judge": "j1", "score": 0.7}, {"model": "j2", "score": 0.5}]}}
|
|
104
|
+
assert extract_judge_scores(c2, "Groundedness") == {"j1": 0.7, "j2": 0.5}
|
|
105
|
+
# case/space-insensitive metric match; aggregate keys (score/spread) are not judges
|
|
106
|
+
c3 = {"scores": {"answer_relevance": {"judge_x": 0.9, "score": 0.9, "spread": 0.1}}}
|
|
107
|
+
assert extract_judge_scores(c3, "Answer Relevance") == {"judge_x": 0.9}
|
|
108
|
+
# metric absent -> empty
|
|
109
|
+
assert extract_judge_scores({"scores": {"other": {"j": 0.5}}}, "groundedness") == {}
|
|
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.2.1 → compass_kb_validation-3.3.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.2.1 → compass_kb_validation-3.3.0}/compass/validators/api_upload.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/base_sparql.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/cross_stage.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/graphdb_metadata.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/neptune_metadata.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/redis_chunks.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/compass/validators/retrieval_quality.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.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
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.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
|
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.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.2.1 → compass_kb_validation-3.3.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.2.1 → compass_kb_validation-3.3.0}/tests/test_universal_concerns.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.2.1 → compass_kb_validation-3.3.0}/tests/test_vectorstore_filters.py
RENAMED
|
File without changes
|