File size: 4,224 Bytes
024c30a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | """Tunables for the knowledge-extraction pipeline.
Every value here was calibrated on real documents and each one has a reason
recorded in KNOWLEDGE_PIPELINE_CALIBRATION.md. Change them deliberately β most
were arrived at by a measurement, and two of them (`FUZZY_MIN_LEN`,
`CACHE_MIN_TOKENS`) fix bugs that are silent when reintroduced.
Label and cue sets live in `config/*.yaml` so they can be tuned without a code
change: label phrasing is the main recall lever and the filter is very sensitive
to it.
"""
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
import yaml
CONFIG_DIR = Path(__file__).resolve().parent / "config"
# ββ Term filter βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Variant C beat both the English-default and Indonesian-phrasing label sets:
# the other two missed the same class (mining activities and materials).
LABELS_VARIANT = "broad"
# 0.25, not 0.35: measured recall 0.854 @ 0.25 vs 0.658 @ 0.35 on `broad`.
# Precision falls (0.41 vs 0.50) and that is the intended trade β the filter is
# deliberately over-inclusive, clustering and ranking absorb the noise, and a
# term the filter never proposes can never be recovered downstream.
SPAN_SCORE_THRESHOLD = 0.25
# The span NER model truncates past ~384 of its own tokens and *warns rather
# than failing*, so a long chunk silently loses its tail. Indonesian technical
# prose subword-tokenises at roughly 2.5x, so 220-word windows still tripped the
# cap; 130 does not. Chunks are fed as overlapping windows with offsets remapped.
WINDOW_WORDS = 130
WINDOW_OVERLAP = 30
SPAN_TOKEN_CAP = 12
# ββ Clustering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FUZZY_THRESHOLD = 92
# Below this length only exact matching is allowed: "PA" and "UA" score highly
# against each other on token_set_ratio. Over-merging is far worse than
# under-merging β an under-merge costs one extra call and one extra review row,
# a wrong merge destroys a distinct term and the expert never sees it.
FUZZY_MIN_LEN = 5
# ββ Evidence ranking ββββββββββββββββββββββββββββββββββββββββββββββββββββ
EVIDENCE_K = 3
CUE_PROXIMITY_CHARS = 100
EVIDENCE_WEIGHTS: dict[str, float] = {
"definitional_cue_near": 5.0,
"term_in_heading": 4.0,
"in_legend_block": 3.5,
"formula_present": 2.0,
"bold_or_italic": 1.5,
"first_occurrence": 1.0,
"tabular_penalty": -3.0,
}
# ββ Chunking ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MAX_CHUNK_TOKENS = 1500
MAX_HEADING_LEN = 90
BOILERPLATE_MIN_FRAC = 0.6
# ββ Extraction ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
TEMPERATURE = 0.0
# OpenAI-family prompt caching does not engage AT ALL below this many prompt
# tokens, so a shorter fixed prefix caches nothing and costs ~10x on input. The
# measured hit rate at/above it was 54%.
CACHE_MIN_TOKENS = 1024
# ββ Validation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MAX_ESCALATION_ROUNDS = 2
CONFLICT_OVERLAP_THRESHOLD = 0.4
DUPLICATE_OVERLAP_THRESHOLD = 0.8
@lru_cache(maxsize=4)
def load_yaml(name: str) -> dict:
with open(CONFIG_DIR / name, encoding="utf-8") as fh:
return yaml.safe_load(fh)
def labels_for(variant: str = LABELS_VARIANT) -> tuple[list[str], float]:
"""Returns (labels, threshold) for a label variant."""
cfg = load_yaml("labels.yaml")
labels = cfg.get(variant) or cfg.get(LABELS_VARIANT) or []
return list(labels), float(cfg.get("threshold", SPAN_SCORE_THRESHOLD))
|