compass-kb-validation 2.3.0__tar.gz → 2.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-2.3.0 → compass_kb_validation-2.4.0}/PKG-INFO +5 -1
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/README.md +4 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/PKG-INFO +5 -1
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/SOURCES.txt +2 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/discover_and_validate.py +19 -8
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/__init__.py +1 -1
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/cli.py +69 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/config/__init__.py +34 -1
- compass_kb_validation-2.4.0/ingestion_validation/grounding.py +341 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/profiler.py +2 -3
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/vectorstore.py +56 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_config.py +25 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_discover_cli.py +11 -0
- compass_kb_validation-2.4.0/tests/test_grounding.py +150 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_vectorstore_filters.py +35 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/LICENSE +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/requires.txt +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/__main__.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/ask.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/baseline.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/chunk_coherence.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/config_lint.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/content_inspect.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/corpus.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/corpus_health.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/diagnostics.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/embedding_drift.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/eval_interop.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/init_wizard.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/live_dashboard.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/mcp_server.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/models/__init__.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/notifications.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/pipeline/__init__.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/provenance.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/__init__.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/embedding.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/graph.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/source.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/providers/storage.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/remediation.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/__init__.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/_report_css.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/_report_js.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/analysis.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/export.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/report/shell.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/retrieval_analysis.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/trend_tracker.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/utils/__init__.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/__init__.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/api_upload.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/base.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/base_sparql.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/cross_stage.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/graphdb_metadata.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/neptune_metadata.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/redis_chunks.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/registry.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/retrieval_quality.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/validators/s3_storage.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/profile_kb.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/pyproject.toml +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/run_validation.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/setup.cfg +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_alt_adapters.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_analysis_report.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_api_upload.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_ask.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_baseline.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_benchmark.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_check_catalog.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_cli.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_config_lint.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_content_inspect.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_corpus.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_corpus_health.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_coverage.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_cross_stage.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_diagnostics.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_discovery_parallel.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_embedding_drift.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_eval_interop.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_gate.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_graph_transport.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_graph_validators.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_init_wizard.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_manifest.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_mcp.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_migration.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_models.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_neptune_bugfixes.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_neptune_bugfixes2.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_notifications.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_pipeline.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_profiler.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_providers.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_redis_chunks.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_registry.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_remediation.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_reporting.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_retrieval_assertions.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_retrieval_quality.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_run_cli.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_sources.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_storage_validator.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_trends.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_universal_concerns.py +0 -0
- {compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compass-kb-validation
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.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
|
|
@@ -228,6 +228,10 @@ compass ask --project my-kb --batch questions.jsonl --format eval --output eval_
|
|
|
228
228
|
# Corpus health (deterministic, no LLM): score chunk quality / find stale + orphan sources
|
|
229
229
|
compass quality --project my-kb
|
|
230
230
|
compass freshness --project my-kb --stale-after 30
|
|
231
|
+
|
|
232
|
+
# Adjudicate an eval's grounding finding against the KB: did the bot's counts match reality?
|
|
233
|
+
# Deterministic - attributes a failure to a layer (retrieval vs the model), never scores answer quality.
|
|
234
|
+
compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
|
|
231
235
|
```
|
|
232
236
|
|
|
233
237
|
`migrate` and `coverage` also render a **self-contained HTML report** (same look as the
|
|
@@ -161,6 +161,10 @@ compass ask --project my-kb --batch questions.jsonl --format eval --output eval_
|
|
|
161
161
|
# Corpus health (deterministic, no LLM): score chunk quality / find stale + orphan sources
|
|
162
162
|
compass quality --project my-kb
|
|
163
163
|
compass freshness --project my-kb --stale-after 30
|
|
164
|
+
|
|
165
|
+
# Adjudicate an eval's grounding finding against the KB: did the bot's counts match reality?
|
|
166
|
+
# Deterministic - attributes a failure to a layer (retrieval vs the model), never scores answer quality.
|
|
167
|
+
compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
|
|
164
168
|
```
|
|
165
169
|
|
|
166
170
|
`migrate` and `coverage` also render a **self-contained HTML report** (same look as the
|
{compass_kb_validation-2.3.0 → compass_kb_validation-2.4.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: 2.
|
|
3
|
+
Version: 2.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
|
|
@@ -228,6 +228,10 @@ compass ask --project my-kb --batch questions.jsonl --format eval --output eval_
|
|
|
228
228
|
# Corpus health (deterministic, no LLM): score chunk quality / find stale + orphan sources
|
|
229
229
|
compass quality --project my-kb
|
|
230
230
|
compass freshness --project my-kb --stale-after 30
|
|
231
|
+
|
|
232
|
+
# Adjudicate an eval's grounding finding against the KB: did the bot's counts match reality?
|
|
233
|
+
# Deterministic - attributes a failure to a layer (retrieval vs the model), never scores answer quality.
|
|
234
|
+
compass grounding-audit --project my-kb --eval-result case.json --fail-on-fabrication
|
|
231
235
|
```
|
|
232
236
|
|
|
233
237
|
`migrate` and `coverage` also render a **self-contained HTML report** (same look as the
|
|
@@ -23,6 +23,7 @@ ingestion_validation/corpus_health.py
|
|
|
23
23
|
ingestion_validation/diagnostics.py
|
|
24
24
|
ingestion_validation/embedding_drift.py
|
|
25
25
|
ingestion_validation/eval_interop.py
|
|
26
|
+
ingestion_validation/grounding.py
|
|
26
27
|
ingestion_validation/init_wizard.py
|
|
27
28
|
ingestion_validation/live_dashboard.py
|
|
28
29
|
ingestion_validation/mcp_server.py
|
|
@@ -82,6 +83,7 @@ tests/test_eval_interop.py
|
|
|
82
83
|
tests/test_gate.py
|
|
83
84
|
tests/test_graph_transport.py
|
|
84
85
|
tests/test_graph_validators.py
|
|
86
|
+
tests/test_grounding.py
|
|
85
87
|
tests/test_init_wizard.py
|
|
86
88
|
tests/test_manifest.py
|
|
87
89
|
tests/test_mcp.py
|
|
@@ -142,8 +142,8 @@ def _resolve_skip_steps(settings, *, include_graphdb: bool, include_retrieval: b
|
|
|
142
142
|
skip += ["GraphDB Metadata", "Neptune Metadata"]
|
|
143
143
|
if not include_retrieval:
|
|
144
144
|
skip.append("Retrieval Quality")
|
|
145
|
-
skip += _normalize_skips(list(getattr(settings, "skip_steps", None) or []))
|
|
146
|
-
skip += _normalize_skips(cli_skip)
|
|
145
|
+
skip += _normalize_skips(list(getattr(settings, "skip_steps", None) or []), source="config")
|
|
146
|
+
skip += _normalize_skips(cli_skip, source="cli")
|
|
147
147
|
|
|
148
148
|
s3_auto_skipped = False
|
|
149
149
|
if settings.discovery.provider in ("vectorstore", "index", "manifest"):
|
|
@@ -155,8 +155,14 @@ def _resolve_skip_steps(settings, *, include_graphdb: bool, include_retrieval: b
|
|
|
155
155
|
return list(dict.fromkeys(skip)), s3_auto_skipped
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
def _normalize_skips(tokens: list[str]) -> list[str]:
|
|
159
|
-
"""Map --skip tokens (step keys or names) to the step names the pipeline skips by.
|
|
158
|
+
def _normalize_skips(tokens: list[str], source: str = "cli") -> list[str]:
|
|
159
|
+
"""Map --skip tokens (step keys or names) to the step names the pipeline skips by.
|
|
160
|
+
|
|
161
|
+
An unknown entry from an interactive ``--skip`` is worth flagging (the user just typed
|
|
162
|
+
it and expects it to work). An unknown entry from the project's ``skip_steps`` is
|
|
163
|
+
config drift (e.g. a step removed in an upgrade) - silent at runtime because it is
|
|
164
|
+
harmless there and firing every run is noise; ``compass lint`` surfaces it once.
|
|
165
|
+
"""
|
|
160
166
|
from ingestion_validation.validators.registry import registered_steps
|
|
161
167
|
specs = registered_steps()
|
|
162
168
|
by_key = {s.key: s.name for s in specs}
|
|
@@ -167,7 +173,7 @@ def _normalize_skips(tokens: list[str]) -> list[str]:
|
|
|
167
173
|
out.append(by_key[t])
|
|
168
174
|
elif t in names:
|
|
169
175
|
out.append(t)
|
|
170
|
-
|
|
176
|
+
elif source == "cli":
|
|
171
177
|
print(f" (skip: unknown step '{t}'; valid keys: {', '.join(sorted(by_key))})")
|
|
172
178
|
return out
|
|
173
179
|
|
|
@@ -508,13 +514,18 @@ def main(argv=None) -> int:
|
|
|
508
514
|
# Corpus-level knowledge-base readiness (completeness, dedup, coverage).
|
|
509
515
|
from ingestion_validation.corpus import analyze_corpus
|
|
510
516
|
|
|
517
|
+
# Read corpus expectations from the canonical `corpus:` block, falling back to the
|
|
518
|
+
# legacy `discovery.extra` location so older configs keep working.
|
|
519
|
+
cc = settings.corpus
|
|
520
|
+
dx = settings.discovery.extra
|
|
511
521
|
expected_documents = _resolve_expected_documents(
|
|
512
|
-
|
|
522
|
+
cc.expected_documents if cc.expected_documents is not None else dx.get("expected_documents"),
|
|
523
|
+
args.project)
|
|
513
524
|
corpus = analyze_corpus(
|
|
514
525
|
documents, results,
|
|
515
526
|
expected_documents=expected_documents,
|
|
516
|
-
expected_folders=
|
|
517
|
-
expected_document_names=
|
|
527
|
+
expected_folders=cc.expected_folders or dx.get("expected_folders"),
|
|
528
|
+
expected_document_names=cc.expected_document_names or dx.get("expected_document_names"),
|
|
518
529
|
readiness_threshold=settings.pipeline.readiness_threshold,
|
|
519
530
|
)
|
|
520
531
|
print(f" KB readiness: {corpus['verdict']} ({corpus['passed']}/{corpus['total']} passed, "
|
|
@@ -1302,6 +1302,72 @@ def cmd_freshness(argv: list[str]) -> int:
|
|
|
1302
1302
|
return 0
|
|
1303
1303
|
|
|
1304
1304
|
|
|
1305
|
+
def cmd_grounding_audit(argv: list[str]) -> int:
|
|
1306
|
+
p = argparse.ArgumentParser(
|
|
1307
|
+
prog="compass grounding-audit",
|
|
1308
|
+
description="Adjudicate an eval's grounding finding against the live KB. Deterministic: "
|
|
1309
|
+
"verifies count/aggregation claims exactly against the KB and attributes the "
|
|
1310
|
+
"failure to a layer (retrieval vs the model). Does not judge answer quality.")
|
|
1311
|
+
p.add_argument("--project")
|
|
1312
|
+
p.add_argument("--env", default="")
|
|
1313
|
+
g = p.add_mutually_exclusive_group(required=True)
|
|
1314
|
+
g.add_argument("--eval-result", metavar="FILE", help="One eval record as JSON.")
|
|
1315
|
+
g.add_argument("--eval-results", metavar="FILE", help="JSONL of eval records (one per line).")
|
|
1316
|
+
p.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
1317
|
+
p.add_argument("--output", help="Write the report to a file (default: stdout).")
|
|
1318
|
+
p.add_argument("--fail-on-fabrication", action="store_true",
|
|
1319
|
+
help="Exit non-zero if any verifiable claim disagrees with the KB.")
|
|
1320
|
+
args = p.parse_args(argv)
|
|
1321
|
+
_health_env(args)
|
|
1322
|
+
|
|
1323
|
+
import json as _json
|
|
1324
|
+
|
|
1325
|
+
from ingestion_validation.config import load_settings, project_config_file
|
|
1326
|
+
from ingestion_validation.grounding import render_report, run_audit
|
|
1327
|
+
from ingestion_validation.providers import get_adapter
|
|
1328
|
+
|
|
1329
|
+
if args.project and project_config_file(args.project) is None:
|
|
1330
|
+
print(f" Project '{args.project}' not found (config/projects/{args.project}.yaml).")
|
|
1331
|
+
return 2
|
|
1332
|
+
settings = load_settings(project=args.project, env=args.env or None)
|
|
1333
|
+
|
|
1334
|
+
# Load the eval record(s).
|
|
1335
|
+
records: list[dict] = []
|
|
1336
|
+
try:
|
|
1337
|
+
if args.eval_result:
|
|
1338
|
+
records = [_json.loads(Path(args.eval_result).read_text(encoding="utf-8"))]
|
|
1339
|
+
else:
|
|
1340
|
+
for line in Path(args.eval_results).read_text(encoding="utf-8").splitlines():
|
|
1341
|
+
if line.strip():
|
|
1342
|
+
records.append(_json.loads(line))
|
|
1343
|
+
except Exception as exc: # noqa: BLE001 - clean message, never a traceback
|
|
1344
|
+
print(f" Could not read eval input: {exc}")
|
|
1345
|
+
return 2
|
|
1346
|
+
|
|
1347
|
+
adapter = get_adapter("vectorstore", settings.redis.provider, config=settings.redis)
|
|
1348
|
+
results = [run_audit(rec, adapter, settings.eval) for rec in records]
|
|
1349
|
+
|
|
1350
|
+
if args.format == "json":
|
|
1351
|
+
from dataclasses import asdict
|
|
1352
|
+
body = _json.dumps([asdict(r) for r in results], indent=2, default=str)
|
|
1353
|
+
else:
|
|
1354
|
+
body = ("\n\n".join(render_report(r, project=args.project or "") for r in results))
|
|
1355
|
+
|
|
1356
|
+
if args.output:
|
|
1357
|
+
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
|
1358
|
+
Path(args.output).write_text(body + "\n", encoding="utf-8")
|
|
1359
|
+
print(f" Wrote {args.output}")
|
|
1360
|
+
else:
|
|
1361
|
+
print(body)
|
|
1362
|
+
|
|
1363
|
+
fabricated = sum(1 for r in results if r.wrong_claims)
|
|
1364
|
+
if fabricated:
|
|
1365
|
+
print(f"\n {fabricated}/{len(results)} case(s) contain claims that disagree with the KB.")
|
|
1366
|
+
if args.fail_on_fabrication:
|
|
1367
|
+
return 1
|
|
1368
|
+
return 0
|
|
1369
|
+
|
|
1370
|
+
|
|
1305
1371
|
# --------------------------------------------------------------------------- #
|
|
1306
1372
|
# compass selftest - prove exit-code propagation in THIS shell (BUG-14/15 diagnosis)
|
|
1307
1373
|
# --------------------------------------------------------------------------- #
|
|
@@ -1355,6 +1421,7 @@ Commands:
|
|
|
1355
1421
|
compare compare retrieval against a ground-truth file (precision/recall/Jaccard)
|
|
1356
1422
|
quality score chunk quality (density, self-containedness, boilerplate) - no LLM
|
|
1357
1423
|
freshness flag stale sources + orphan documents (source object gone)
|
|
1424
|
+
grounding-audit check an eval's grounding finding against the KB (count claims) - no LLM
|
|
1358
1425
|
demo generate a sample report from synthetic data (no backend needed)
|
|
1359
1426
|
init [name] scaffold a project (config + .env.example + marker). -i/--interactive wizard; --graph graph block; --ci workflow
|
|
1360
1427
|
lint statically check a project config for silent-failure traps (no backend)
|
|
@@ -1399,6 +1466,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1399
1466
|
return cmd_compare(rest)
|
|
1400
1467
|
if cmd == "quality":
|
|
1401
1468
|
return cmd_quality(rest)
|
|
1469
|
+
if cmd == "grounding-audit":
|
|
1470
|
+
return cmd_grounding_audit(rest)
|
|
1402
1471
|
if cmd == "freshness":
|
|
1403
1472
|
return cmd_freshness(rest)
|
|
1404
1473
|
if cmd == "selftest":
|
{compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/config/__init__.py
RENAMED
|
@@ -534,6 +534,22 @@ class DiscoveryConfig:
|
|
|
534
534
|
extra: dict[str, Any] = field(default_factory=dict)
|
|
535
535
|
|
|
536
536
|
|
|
537
|
+
@dataclass
|
|
538
|
+
class CorpusConfig:
|
|
539
|
+
"""Corpus-level expectations: what SHOULD be in the knowledge base.
|
|
540
|
+
|
|
541
|
+
The natural home for the expected document set, so completeness alerts on drift. This
|
|
542
|
+
is the block users reach for (`corpus:`); previously it was neither recognized nor read
|
|
543
|
+
(the values had to live under `discovery.extra`, which few discover), so a
|
|
544
|
+
`corpus.expected_documents` was silently ignored. Now first-class and read directly.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
expected_documents: Any = None # int, or "auto" (baseline from last run)
|
|
548
|
+
expected_folders: list[str] = field(default_factory=list)
|
|
549
|
+
expected_document_names: list[str] = field(default_factory=list)
|
|
550
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
551
|
+
|
|
552
|
+
|
|
537
553
|
@dataclass
|
|
538
554
|
class EmbeddingConfig:
|
|
539
555
|
"""How to turn a natural-language query into a vector for `compass ask`.
|
|
@@ -582,6 +598,21 @@ class EvalConfig:
|
|
|
582
598
|
gt_variant_path: str = ""
|
|
583
599
|
gt_variant_filter: list[str] = field(default_factory=list)
|
|
584
600
|
|
|
601
|
+
# --- Grounding audit (compass grounding-audit) ------------------------------------
|
|
602
|
+
# Verify an eval's grounding finding against the live KB. Every field name here comes
|
|
603
|
+
# from config, never assumed - a KB whose ingestion named things differently only
|
|
604
|
+
# changes these values, not code.
|
|
605
|
+
gt_response_path: str = "response" # the answer text in the eval record
|
|
606
|
+
gt_context_path: str = "context.knowledge_retrieval" # the retrieved context the eval saw
|
|
607
|
+
gt_scores_path: str = "scores" # eval scores (shown in the report)
|
|
608
|
+
# Structured-query verification: which fields the KB can aggregate to check a count
|
|
609
|
+
# claim. Each entry: {field: <group-by field>, distinct_by: <doc-identity field>,
|
|
610
|
+
# label: <optional human label>}. Empty = count claims are not verified.
|
|
611
|
+
aggregatable_fields: list[dict] = field(default_factory=list)
|
|
612
|
+
# Extra query-intent regexes, merged with the built-in defaults (how many / count / by
|
|
613
|
+
# type / list all / most recent ...). Lets a domain add its own phrasings.
|
|
614
|
+
structured_query_patterns: list[str] = field(default_factory=list)
|
|
615
|
+
|
|
585
616
|
|
|
586
617
|
@dataclass
|
|
587
618
|
class ConsistencyConfig:
|
|
@@ -640,6 +671,7 @@ class Settings:
|
|
|
640
671
|
embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig)
|
|
641
672
|
eval: EvalConfig = field(default_factory=EvalConfig)
|
|
642
673
|
consistency: ConsistencyConfig = field(default_factory=ConsistencyConfig)
|
|
674
|
+
corpus: CorpusConfig = field(default_factory=CorpusConfig)
|
|
643
675
|
# Non-service metadata carried through from config files.
|
|
644
676
|
project_name: str = ""
|
|
645
677
|
display_name: str = ""
|
|
@@ -661,7 +693,7 @@ _META_KEYS = frozenset({"default_env", "display_name", "environments", "skip_ste
|
|
|
661
693
|
# new block can never be added to one and forgotten in the other (which is exactly how
|
|
662
694
|
# `consistency` came to be silently dropped at the top level).
|
|
663
695
|
_SERVICE_BLOCKS = ("api", "s3", "graphdb", "redis", "neptune", "pipeline",
|
|
664
|
-
"retrieval", "discovery", "embedding", "eval", "consistency")
|
|
696
|
+
"retrieval", "discovery", "embedding", "eval", "consistency", "corpus")
|
|
665
697
|
|
|
666
698
|
|
|
667
699
|
def _deep_merge(base: dict, overlay: dict) -> dict:
|
|
@@ -787,6 +819,7 @@ def load_settings(
|
|
|
787
819
|
embedding=_hydrate(EmbeddingConfig, flat.get("embedding")),
|
|
788
820
|
eval=_hydrate(EvalConfig, flat.get("eval")),
|
|
789
821
|
consistency=_hydrate(ConsistencyConfig, flat.get("consistency")),
|
|
822
|
+
corpus=_hydrate(CorpusConfig, flat.get("corpus")),
|
|
790
823
|
project_name=project or "",
|
|
791
824
|
display_name=merged.get("display_name", ""),
|
|
792
825
|
environment=resolved_env,
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Grounding audit: adjudicate an eval's grounding finding against the live KB.
|
|
2
|
+
|
|
3
|
+
An eval (often an LLM judge) flags "this answer looks ungrounded". That verdict can be
|
|
4
|
+
wrong, so this module supplies the DETERMINISTIC ground truth the verdict is checked
|
|
5
|
+
against: it queries the KB COMPASS already knows how to reach and asks factual, checkable
|
|
6
|
+
questions - is this claim actually in the KB, and did retrieval surface it? - then
|
|
7
|
+
attributes the failure to a layer (retrieval vs the model). It does NOT score answer
|
|
8
|
+
quality; that stays in the eval layer. No LLM in any verdict path.
|
|
9
|
+
|
|
10
|
+
Phase 1 (this module) implements the STRUCTURED path - count/aggregation claims verified
|
|
11
|
+
exactly against the KB via the adapter's ``aggregate_count`` (e.g. "the bot said 24
|
|
12
|
+
documents; the KB has 127"). It is deterministic and unambiguous. The semantic-claim path
|
|
13
|
+
(free-text claim extraction) is a separate, measurement-gated effort; see ``run_audit``.
|
|
14
|
+
|
|
15
|
+
Backend-agnostic and schema-agnostic by construction: field names come from config, and
|
|
16
|
+
aggregation goes through the adapter - a KB whose ingestion is shaped differently only
|
|
17
|
+
changes config values, never this code. Backends that cannot aggregate exactly return
|
|
18
|
+
None and the report says so, rather than asserting a wrong actual count.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
|
|
27
|
+
# Built-in query-intent patterns. A KB can add its own via eval.structured_query_patterns;
|
|
28
|
+
# these are merged, never replaced, so the defaults always hold.
|
|
29
|
+
_STRUCTURED_PATTERNS: tuple[tuple[str, str], ...] = (
|
|
30
|
+
(r"\bhow many\b", "count"),
|
|
31
|
+
(r"\bcount\b", "count"),
|
|
32
|
+
(r"\bnumber of\b", "count"),
|
|
33
|
+
(r"\btotal\b.*\b(document|doc|record|item|file)", "count"),
|
|
34
|
+
(r"\bby type\b", "groupby"),
|
|
35
|
+
(r"\bbreakdown\b", "groupby"),
|
|
36
|
+
(r"\bwhat types\b", "groupby"),
|
|
37
|
+
(r"\blist all\b", "enumerate"),
|
|
38
|
+
(r"\bmost recent\b", "sort"),
|
|
39
|
+
(r"\blatest\b", "sort"),
|
|
40
|
+
(r"\boldest\b", "sort"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def classify_query(query: str, extra_patterns: Optional[list[str]] = None) -> tuple[str, list[str]]:
|
|
45
|
+
"""Classify a query as 'structured' (aggregation/count/list) or 'semantic' (topic).
|
|
46
|
+
|
|
47
|
+
Deterministic regex intent detection. Returns (kind, sorted-unique-intents).
|
|
48
|
+
"""
|
|
49
|
+
q = (query or "").lower()
|
|
50
|
+
intents: set[str] = set()
|
|
51
|
+
for pattern, intent in _STRUCTURED_PATTERNS:
|
|
52
|
+
if re.search(pattern, q):
|
|
53
|
+
intents.add(intent)
|
|
54
|
+
for pattern in extra_patterns or []:
|
|
55
|
+
try:
|
|
56
|
+
if re.search(pattern, q):
|
|
57
|
+
intents.add("count") # user-declared structured phrasing
|
|
58
|
+
except re.error:
|
|
59
|
+
continue
|
|
60
|
+
return ("structured" if intents else "semantic"), sorted(intents)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class NumericClaim:
|
|
65
|
+
label: str # what the count is about (a group value, or "total")
|
|
66
|
+
count: int # the number the answer asserted
|
|
67
|
+
source: str # how it was extracted (table | inline)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_numeric_claims(response: str) -> list[NumericClaim]:
|
|
71
|
+
"""Extract (label, count) claims from an answer - generically, no hardcoded labels.
|
|
72
|
+
|
|
73
|
+
Labels are whatever the answer names (a document type, a category, ...); they are
|
|
74
|
+
matched against real KB values later, never against a fixed set. Catches markdown
|
|
75
|
+
table rows and inline "N documents" phrasings.
|
|
76
|
+
"""
|
|
77
|
+
text = response or ""
|
|
78
|
+
claims: list[NumericClaim] = []
|
|
79
|
+
seen: set[tuple[str, int]] = set()
|
|
80
|
+
|
|
81
|
+
def _add(label: str, count: int, source: str) -> None:
|
|
82
|
+
label = label.strip().strip("*").strip()
|
|
83
|
+
key = (label.lower(), count)
|
|
84
|
+
if label and key not in seen:
|
|
85
|
+
seen.add(key)
|
|
86
|
+
claims.append(NumericClaim(label=label, count=count, source=source))
|
|
87
|
+
|
|
88
|
+
# Markdown table rows: first cell is the label, the last standalone integer cell is
|
|
89
|
+
# the count. Header/separator rows (label in a known non-data set) are skipped.
|
|
90
|
+
_skip = {"type", "total", "metric", "count", "category", "documents", "document type", ""}
|
|
91
|
+
for row in re.findall(r"^\s*\|(.+)\|\s*$", text, flags=re.MULTILINE):
|
|
92
|
+
cells = [c.strip().strip("*").strip() for c in row.split("|")]
|
|
93
|
+
if not cells or all(set(c) <= set("-: ") for c in cells):
|
|
94
|
+
continue # separator row
|
|
95
|
+
label = cells[0]
|
|
96
|
+
if label.lower() in _skip:
|
|
97
|
+
continue
|
|
98
|
+
nums = [int(c.replace(",", "")) for c in cells[1:] if re.fullmatch(r"[\d,]+", c)]
|
|
99
|
+
if nums:
|
|
100
|
+
_add(label, nums[-1], "table")
|
|
101
|
+
|
|
102
|
+
# Inline "N documents / docs / files / records / PDFs".
|
|
103
|
+
for m in re.finditer(r"\*{0,2}([\d,]+)\*{0,2}\s+(?:total\s+)?(?:documents?|docs|files?|records?|pdfs?)\b",
|
|
104
|
+
text, flags=re.IGNORECASE):
|
|
105
|
+
_add("total", int(m.group(1).replace(",", "")), "inline")
|
|
106
|
+
|
|
107
|
+
return claims
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class ClaimVerdict:
|
|
112
|
+
label: str
|
|
113
|
+
claimed: int
|
|
114
|
+
actual: Optional[int] # None = not verifiable (backend can't aggregate / field absent)
|
|
115
|
+
verifiable: bool
|
|
116
|
+
correct: bool
|
|
117
|
+
detail: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def verify_structured_claims(adapter: Any, claims: list[NumericClaim],
|
|
121
|
+
aggregatable_fields: list[dict]) -> tuple[list[ClaimVerdict], dict]:
|
|
122
|
+
"""Verify count claims against exact KB aggregations via the adapter.
|
|
123
|
+
|
|
124
|
+
Returns (verdicts, meta). ``meta`` carries why verification could not run (e.g. the
|
|
125
|
+
backend does not support aggregation) so the report degrades honestly instead of
|
|
126
|
+
asserting a wrong actual value.
|
|
127
|
+
"""
|
|
128
|
+
meta: dict = {"aggregated": False, "reason": ""}
|
|
129
|
+
if not getattr(adapter, "supports_aggregation", False):
|
|
130
|
+
meta["reason"] = (f"{type(adapter).__name__} does not support exact aggregation - "
|
|
131
|
+
"count claims cannot be verified on this backend.")
|
|
132
|
+
return [ClaimVerdict(c.label, c.count, None, False, False, "backend cannot aggregate")
|
|
133
|
+
for c in claims], meta
|
|
134
|
+
if not aggregatable_fields:
|
|
135
|
+
meta["reason"] = ("no eval.aggregatable_fields configured - declare the group-by and "
|
|
136
|
+
"doc-identity fields to verify count claims.")
|
|
137
|
+
return [ClaimVerdict(c.label, c.count, None, False, False, "no aggregatable fields configured")
|
|
138
|
+
for c in claims], meta
|
|
139
|
+
|
|
140
|
+
# Build a value -> count map across every configured aggregation, plus an overall
|
|
141
|
+
# total (distinct documents) for "total" claims.
|
|
142
|
+
value_counts: dict[str, int] = {}
|
|
143
|
+
total_docs = 0
|
|
144
|
+
for spec in aggregatable_fields:
|
|
145
|
+
gfield = spec.get("field", "")
|
|
146
|
+
dfield = spec.get("distinct_by") or None
|
|
147
|
+
counts = adapter.aggregate_count(gfield, dfield) if gfield else None
|
|
148
|
+
if counts:
|
|
149
|
+
meta["aggregated"] = True
|
|
150
|
+
for k, v in counts.items():
|
|
151
|
+
value_counts[str(k).lower()] = value_counts.get(str(k).lower(), 0) + int(v)
|
|
152
|
+
total_docs += int(v)
|
|
153
|
+
if not meta["aggregated"]:
|
|
154
|
+
meta["reason"] = "aggregation returned nothing (empty index, or the configured fields are absent)."
|
|
155
|
+
|
|
156
|
+
verdicts: list[ClaimVerdict] = []
|
|
157
|
+
for c in claims:
|
|
158
|
+
if c.label.lower() == "total":
|
|
159
|
+
actual = total_docs if meta["aggregated"] else None
|
|
160
|
+
else:
|
|
161
|
+
actual = value_counts.get(c.label.lower())
|
|
162
|
+
if actual is None:
|
|
163
|
+
verdicts.append(ClaimVerdict(c.label, c.count, None, False, False,
|
|
164
|
+
"no matching KB value" if meta["aggregated"] else meta["reason"]))
|
|
165
|
+
continue
|
|
166
|
+
correct = actual == c.count
|
|
167
|
+
if correct:
|
|
168
|
+
detail = "matches KB"
|
|
169
|
+
else:
|
|
170
|
+
ratio = (actual / c.count) if c.count else float("inf")
|
|
171
|
+
direction = "undercount" if actual > c.count else "overcount"
|
|
172
|
+
detail = f"off by {abs(actual - c.count)} ({ratio:.1f}x {direction})"
|
|
173
|
+
verdicts.append(ClaimVerdict(c.label, c.count, actual, True, correct, detail))
|
|
174
|
+
return verdicts, meta
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _is_empty_context(ctx: Any) -> bool:
|
|
178
|
+
"""True when the eval's retrieved context is effectively empty (null/blank fields)."""
|
|
179
|
+
if not ctx:
|
|
180
|
+
return True
|
|
181
|
+
s = str(ctx).strip()
|
|
182
|
+
return s in ("", "{}", "[]", "null", "None") or bool(re.fullmatch(r"[\s{}\[\]:,'\"]*", s))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class GroundingResult:
|
|
187
|
+
test_id: str
|
|
188
|
+
query: str
|
|
189
|
+
query_type: str
|
|
190
|
+
intents: list[str]
|
|
191
|
+
claim_verdicts: list[ClaimVerdict] = field(default_factory=list)
|
|
192
|
+
context_empty: bool = True
|
|
193
|
+
eval_scores: dict = field(default_factory=dict)
|
|
194
|
+
meta: dict = field(default_factory=dict)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def wrong_claims(self) -> list[ClaimVerdict]:
|
|
198
|
+
return [v for v in self.claim_verdicts if v.verifiable and not v.correct]
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def verified_claims(self) -> list[ClaimVerdict]:
|
|
202
|
+
return [v for v in self.claim_verdicts if v.verifiable]
|
|
203
|
+
|
|
204
|
+
def layer_diagnosis(self) -> str:
|
|
205
|
+
"""Deterministic layer attribution - which stage is implicated, no quality score."""
|
|
206
|
+
if self.query_type == "structured" and self.wrong_claims:
|
|
207
|
+
if self.context_empty:
|
|
208
|
+
return ("HALLUCINATION + QUERY-ROUTING FAILURE: retrieval returned nothing and the "
|
|
209
|
+
"model fabricated counts. An aggregation query was answered by semantic search.")
|
|
210
|
+
return "FABRICATION: the model's counts disagree with the KB despite retrieved context."
|
|
211
|
+
if self.query_type == "structured" and self.verified_claims and not self.wrong_claims:
|
|
212
|
+
return "GROUNDED: every verifiable count matches the KB."
|
|
213
|
+
if not self.verified_claims:
|
|
214
|
+
return "UNVERIFIED: no count claim could be checked (see notes)."
|
|
215
|
+
return "MIXED: some counts match, others do not."
|
|
216
|
+
|
|
217
|
+
def severity(self) -> str:
|
|
218
|
+
if self.wrong_claims:
|
|
219
|
+
return "HIGH" # confident wrong data reaching users
|
|
220
|
+
if not self.verified_claims:
|
|
221
|
+
return "LOW"
|
|
222
|
+
return "INFO"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_path(obj: Any, dotted: str) -> Any:
|
|
226
|
+
"""Read a dotted path (e.g. 'context.knowledge_retrieval') from a nested dict."""
|
|
227
|
+
cur = obj
|
|
228
|
+
for part in (dotted or "").split("."):
|
|
229
|
+
if isinstance(cur, dict) and part in cur:
|
|
230
|
+
cur = cur[part]
|
|
231
|
+
else:
|
|
232
|
+
return None
|
|
233
|
+
return cur
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def run_audit(eval_record: dict, adapter: Any, eval_cfg: Any) -> GroundingResult:
|
|
237
|
+
"""Adjudicate one eval record against the KB. Deterministic; the structured path only.
|
|
238
|
+
|
|
239
|
+
``eval_cfg`` supplies the dotted paths (gt_question_path, gt_response_path,
|
|
240
|
+
gt_context_path, gt_scores_path) and ``aggregatable_fields`` - all schema-agnostic.
|
|
241
|
+
"""
|
|
242
|
+
query = str(get_path(eval_record, eval_cfg.gt_question_path) or "")
|
|
243
|
+
response = str(get_path(eval_record, getattr(eval_cfg, "gt_response_path", "response")) or "")
|
|
244
|
+
context = get_path(eval_record, getattr(eval_cfg, "gt_context_path", "context.knowledge_retrieval"))
|
|
245
|
+
scores = get_path(eval_record, getattr(eval_cfg, "gt_scores_path", "scores")) or {}
|
|
246
|
+
test_id = str(eval_record.get("test_id") or eval_record.get("id") or "")
|
|
247
|
+
|
|
248
|
+
qtype, intents = classify_query(query, getattr(eval_cfg, "structured_query_patterns", []))
|
|
249
|
+
result = GroundingResult(test_id=test_id, query=query, query_type=qtype, intents=intents,
|
|
250
|
+
context_empty=_is_empty_context(context),
|
|
251
|
+
eval_scores=scores if isinstance(scores, dict) else {})
|
|
252
|
+
if qtype == "structured":
|
|
253
|
+
claims = extract_numeric_claims(response)
|
|
254
|
+
result.claim_verdicts, result.meta = verify_structured_claims(
|
|
255
|
+
adapter, claims, getattr(eval_cfg, "aggregatable_fields", []) or [])
|
|
256
|
+
else:
|
|
257
|
+
# Semantic path (free-text claim verification) is a separate, measurement-gated
|
|
258
|
+
# mode; for now say so rather than run a fragile extractor and imply a verdict.
|
|
259
|
+
result.meta = {"semantic": True,
|
|
260
|
+
"note": "Semantic claim verification is not yet enabled; only the "
|
|
261
|
+
"retrieved-context check ran."}
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def render_report(result: GroundingResult, *, project: str = "") -> str:
|
|
266
|
+
"""Render a portable, evidence-first defect report (issue tracker / wiki / chat ready).
|
|
267
|
+
|
|
268
|
+
COMPASS's authoritative content is the evidence (claimed vs actual, from the KB) and
|
|
269
|
+
the deterministic layer attribution. The recommendations are clearly-labelled generic
|
|
270
|
+
hints, never presented as bespoke analysis or an answer-quality score.
|
|
271
|
+
"""
|
|
272
|
+
L: list[str] = []
|
|
273
|
+
diagnosis = result.layer_diagnosis()
|
|
274
|
+
L.append(f"## Grounding Audit: {result.test_id or 'case'} - \"{result.query}\"")
|
|
275
|
+
L.append("")
|
|
276
|
+
L.append(f"**Severity:** {result.severity()} ")
|
|
277
|
+
L.append(f"**Query type:** {result.query_type}"
|
|
278
|
+
+ (f" ({', '.join(result.intents)})" if result.intents else "") + " ")
|
|
279
|
+
L.append(f"**Verdict (deterministic, KB-verified):** {diagnosis}")
|
|
280
|
+
L.append("")
|
|
281
|
+
|
|
282
|
+
if result.eval_scores:
|
|
283
|
+
L.append("### Eval scores (as reported by the eval - shown for context)")
|
|
284
|
+
L.append("")
|
|
285
|
+
L.append("| Metric | Score | Target | Status |")
|
|
286
|
+
L.append("|---|---|---|---|")
|
|
287
|
+
for metric, data in result.eval_scores.items():
|
|
288
|
+
if isinstance(data, dict):
|
|
289
|
+
score, target = data.get("score", 0), data.get("target", 0)
|
|
290
|
+
else:
|
|
291
|
+
score, target = data, 0
|
|
292
|
+
status = "PASS" if (isinstance(score, (int, float)) and score >= target) else "FAIL"
|
|
293
|
+
L.append(f"| {metric} | {score} | {target} | {status} |")
|
|
294
|
+
L.append("")
|
|
295
|
+
|
|
296
|
+
if result.claim_verdicts:
|
|
297
|
+
L.append("### Claim verification against the KB")
|
|
298
|
+
L.append("")
|
|
299
|
+
L.append("| Claim | Bot said | KB actual | Result |")
|
|
300
|
+
L.append("|---|---|---|---|")
|
|
301
|
+
for v in result.claim_verdicts:
|
|
302
|
+
actual = v.actual if v.actual is not None else "unverifiable"
|
|
303
|
+
mark = "OK" if v.correct else ("FAIL" if v.verifiable else "N/A")
|
|
304
|
+
L.append(f"| {v.label} | {v.claimed} | {actual} | {mark} - {v.detail} |")
|
|
305
|
+
L.append("")
|
|
306
|
+
n_ok = sum(1 for v in result.claim_verdicts if v.correct)
|
|
307
|
+
n_ver = len(result.verified_claims)
|
|
308
|
+
if n_ver:
|
|
309
|
+
L.append(f"**{n_ok}/{n_ver} verifiable claim(s) match the KB.**")
|
|
310
|
+
if result.meta.get("reason"):
|
|
311
|
+
L.append(f"> Note: {result.meta['reason']}")
|
|
312
|
+
L.append("")
|
|
313
|
+
|
|
314
|
+
L.append("### Retrieved context")
|
|
315
|
+
L.append("")
|
|
316
|
+
L.append("`[EMPTY - retrieval returned nothing]`" if result.context_empty
|
|
317
|
+
else "_(non-empty; see the eval record)_")
|
|
318
|
+
L.append("")
|
|
319
|
+
|
|
320
|
+
# Recommendations: generic, template-selected by the diagnosis. Labelled as hints.
|
|
321
|
+
L.append("### Suggested next steps (generic guidance, not KB-specific analysis)")
|
|
322
|
+
L.append("")
|
|
323
|
+
if result.query_type == "structured" and result.wrong_claims:
|
|
324
|
+
L += [
|
|
325
|
+
"- Route count/aggregation questions to a **direct aggregation** (the KB can answer",
|
|
326
|
+
" these exactly), not semantic search.",
|
|
327
|
+
"- On **empty retrieval, refuse or flag uncertainty** rather than letting the model fill the gap.",
|
|
328
|
+
]
|
|
329
|
+
elif result.query_type == "semantic":
|
|
330
|
+
L += [
|
|
331
|
+
"- This is a topic query; run the semantic grounding mode (when enabled) to verify",
|
|
332
|
+
" individual claims against the KB.",
|
|
333
|
+
]
|
|
334
|
+
else:
|
|
335
|
+
L.append("- Counts verified against the KB; no action indicated for this case.")
|
|
336
|
+
L.append("")
|
|
337
|
+
L.append("---")
|
|
338
|
+
L.append("_Generated by `compass grounding-audit`"
|
|
339
|
+
+ (f" (project {project})" if project else "") + ". "
|
|
340
|
+
"Evidence is deterministic and KB-verified; it does not judge answer quality._")
|
|
341
|
+
return "\n".join(L) + "\n"
|
{compass_kb_validation-2.3.0 → compass_kb_validation-2.4.0}/ingestion_validation/profiler.py
RENAMED
|
@@ -581,9 +581,8 @@ def suggest_config_from_run(results: list, documents: list) -> str:
|
|
|
581
581
|
"",
|
|
582
582
|
"# Documents discovered this run. Pin it to alert on drift (or use 'auto' to",
|
|
583
583
|
"# baseline against the previous run automatically):",
|
|
584
|
-
"
|
|
585
|
-
"
|
|
586
|
-
f" expected_documents: {n_docs}",
|
|
584
|
+
"corpus:",
|
|
585
|
+
f" expected_documents: {n_docs}",
|
|
587
586
|
]
|
|
588
587
|
if avg_chars:
|
|
589
588
|
median_size = int(statistics.median(avg_chars))
|