feat: tier-0 document classifier in shadow mode
First step of the tiered document-processor effort (Deck #203): a cheap, local pre-pass that recommends which extraction tier a PDF should start in, emitting metrics WITHOUT changing routing yet -- so we gather per-tenant doc-mix data before turning escalation on. document_processors/classifier.py: classify_pdf(content) -> DocClassification. Page-sampled (bounded on large docs), <~1s. Cheap signals only -- text-layer chars, a text-quality score (catches the "Student 147" failure where a text layer exists but is mashed/space-less junk), and image coverage. A page that is mostly a raster image routes to OCR: its content (handwriting, stamps) isn't in any text layer. Deliberately no get_drawings/graphics-density signal -- it's slow on the exact pages it'd flag, the hotfix's graphics_limit already makes the parse safe, and the (future) tier-1 quality gate catches lost tables. Validated on the sample corpus: born-digital 2-col arxiv and a digital student record -> fast (tier 1); a scanned+handwritten form -> ocr (tier 3). Wiring (vector/processor.py): _shadow_classify runs the classifier on PDFs in a worker thread, best-effort (never blocks/fails indexing), gated by the new DOCUMENT_CLASSIFY_ENABLED setting. Metrics: astrolabe_document_classified_total {recommended_tier}, astrolabe_document_classifier_flag_total{flag}, astrolabe_document_text_quality histogram. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
dd335275ac
commit
044c1da750
@@ -272,6 +272,30 @@ document_parse_failed_total = Counter(
|
||||
["reason"], # reason: timeout | oom | error
|
||||
)
|
||||
|
||||
# --- Tier-0 classifier (shadow mode) -----------------------------------------
|
||||
#
|
||||
# The classifier runs a cheap pre-pass per PDF and recommends a starting tier.
|
||||
# In shadow mode it changes no routing -- these metrics gather the per-tenant
|
||||
# doc-mix needed to tune the thresholds before routing is enabled.
|
||||
|
||||
document_classified_total = Counter(
|
||||
"astrolabe_document_classified_total",
|
||||
"Documents classified by tier-0, by recommended starting tier",
|
||||
["recommended_tier"], # fast | ocr
|
||||
)
|
||||
|
||||
document_classifier_flag_total = Counter(
|
||||
"astrolabe_document_classifier_flag_total",
|
||||
"Tier-0 classifier flags raised on documents",
|
||||
["flag"], # image_heavy | scanned | bad_text_layer
|
||||
)
|
||||
|
||||
document_text_quality = Histogram(
|
||||
"astrolabe_document_text_quality",
|
||||
"Tier-0 mean text-layer quality per document (0=junk, 1=clean prose)",
|
||||
buckets=(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0),
|
||||
)
|
||||
|
||||
# --- Embedding stages ---------------------------------------------------------
|
||||
|
||||
embedding_duration_seconds = Histogram(
|
||||
@@ -637,6 +661,20 @@ def record_document_parse_failed(reason: str) -> None:
|
||||
document_parse_failed_total.labels(reason=reason).inc()
|
||||
|
||||
|
||||
def record_document_classification(
|
||||
recommended_tier: str, flags: set[str], mean_text_quality: float
|
||||
) -> None:
|
||||
"""Record a tier-0 classification result (shadow mode -- observability only).
|
||||
|
||||
Primitive args (not the DocClassification object) keep the observability
|
||||
layer free of a dependency on document_processors.
|
||||
"""
|
||||
document_classified_total.labels(recommended_tier=recommended_tier).inc()
|
||||
for flag in flags:
|
||||
document_classifier_flag_total.labels(flag=flag).inc()
|
||||
document_text_quality.observe(mean_text_quality)
|
||||
|
||||
|
||||
def record_embedding(
|
||||
kind: str,
|
||||
provider: str,
|
||||
|
||||
Reference in New Issue
Block a user