qa/source_roles: extract _classify_source_role + weights to own module
Phase 1 step 1 of #53 (collapse legacy 2000-line query() into the unified run_query orchestrator). Pure code motion — no behavior change, no signature change. Lifts: - SOURCE_ROLE_BUDGET_WEIGHTS / SOURCE_ROLE_RANK_WEIGHTS - _NOISY/_SEQUEL/_SECONDARY title-marker tuples - _classify_source_role function from query.py:781-854,1579-1591 to a new arborist/qa/source_roles.py module. query.py re-exports under the old names so existing imports (and the 45-test test_query.py suite) keep working. Goal: run_query needs the source-role classifier to do role-weighted context-budget splits (Plan §5 Phase 1 deliverable 1). Until this file exists, run_query has no import path to it that doesn't pull in the full 4280-line query.py module. Lazy-imports `_title_query_tokens` and `_stem_token_for_match` from query.py for now — those move to _text_norm.py in a later Phase 1 step. Validation: 45 tests in test_query.py pass; 257 tests in the broader query/corpus/sidecar/wallet/bucket/claim_lattice gate pass.
This commit is contained in:
parent
2b8c12303b
commit
9ba6317382
2 changed files with 124 additions and 92 deletions
|
|
@ -772,86 +772,17 @@ class _Hit:
|
|||
source_role: str = "unclassified"
|
||||
|
||||
|
||||
# Heuristic role classification + per-role budget multiplier. Lets the
|
||||
# primary answer page (e.g. `Jurassic Park (film)` for a JP film query)
|
||||
# claim a wider context slice than peripheral pages (`Jurassic Park (film
|
||||
# score)`, `Jurassic Park video games`). The running `char_budget` check
|
||||
# still bounds total context to `max_context_chars`; weights just shift
|
||||
# how the budget gets divided.
|
||||
SOURCE_ROLE_BUDGET_WEIGHTS = {
|
||||
"primary_answer_source": 2.0,
|
||||
"secondary_context_source": 1.0,
|
||||
"noisy_background_source": 0.5,
|
||||
"sequel_background_source": 0.5,
|
||||
"background_source": 1.0,
|
||||
"unclassified": 1.0,
|
||||
# Self-promoted providence records (STRICT live, past kindergarten
|
||||
# window). Same budget weight as background — Wikipedia stays the
|
||||
# canonical primary; self-reference is supplementary anchoring.
|
||||
# Trust model: STRICT-as-fact unless the verifier falsifies it.
|
||||
"self_reference_source": 1.0,
|
||||
}
|
||||
|
||||
# Title patterns that demote a source's role. Lower-cased substring match.
|
||||
# 2026-04-30: extended to catch tie-in spinoff titles. The JP-dinosaurs
|
||||
# query lazy-anchored on "Jurassic Park: Operation Genesis" (a video game)
|
||||
# whose enumerative dinosaur tables pattern-matched the question shape
|
||||
# more cleanly than the actual film article's prose. Adding the explicit
|
||||
# game subtitle plus generic markers ("the game", "video games") so
|
||||
# similar tie-ins classify as noisy and drop out of the evidence map.
|
||||
_NOISY_TITLE_MARKERS = (
|
||||
"score", "music", "soundtrack", "video game", "video games",
|
||||
"merchandise", "discography", "operation genesis", "the game",
|
||||
# Source-role classifier + per-role weighting now live in
|
||||
# arborist.qa.source_roles. Re-export under the old names so callers
|
||||
# (and tests) that import from here keep working until the rest of
|
||||
# Phase 1 lands.
|
||||
from arborist.qa.source_roles import (
|
||||
SOURCE_ROLE_BUDGET_WEIGHTS,
|
||||
classify_source_role as _classify_source_role,
|
||||
_NOISY_TITLE_MARKERS,
|
||||
_SEQUEL_TITLE_MARKERS,
|
||||
_SECONDARY_TITLE_MARKERS,
|
||||
)
|
||||
_SEQUEL_TITLE_MARKERS = (
|
||||
" iii", " ii)", " ii ", " iv", " v ", " v)", "lost world", "sequel",
|
||||
" 2)", " 3)", " 4)",
|
||||
)
|
||||
_SECONDARY_TITLE_MARKERS = (
|
||||
"list of", "characters", "franchise", "history of", "people",
|
||||
"timeline of",
|
||||
)
|
||||
|
||||
|
||||
def _classify_source_role(
|
||||
title: str | None,
|
||||
qtokens_stem: set[str],
|
||||
*,
|
||||
document_uri: str | None = None,
|
||||
) -> str:
|
||||
"""Tag a source by its likely role for an N-token query.
|
||||
|
||||
URI-scheme classification fires first: documents whose URI starts
|
||||
with ``arborist://providence/`` are self-promoted providence
|
||||
records (per ``arborist/sources/providence.py``) and classify as
|
||||
``self_reference_source`` regardless of title shape — that role
|
||||
captures the trust model "STRICT-as-fact unless verifier
|
||||
falsifies."
|
||||
|
||||
Order matters for the title-based fallback: noisy/sequel/secondary
|
||||
markers fire first because they catch peripheral pages whose
|
||||
titles otherwise overlap query tokens fully (e.g. `Jurassic Park
|
||||
(film score)` shares 3 stems with `{dinosaur, jurassic, park,
|
||||
film}` but is not the primary answer source for a dinosaurs
|
||||
question). Primary requires the strongest title coverage (N-1
|
||||
of N stems present).
|
||||
"""
|
||||
if document_uri and document_uri.startswith("arborist://providence/"):
|
||||
return "self_reference_source"
|
||||
if not title:
|
||||
return "unclassified"
|
||||
t = title.lower()
|
||||
if any(k in t for k in _NOISY_TITLE_MARKERS):
|
||||
return "noisy_background_source"
|
||||
if any(k in t for k in _SEQUEL_TITLE_MARKERS):
|
||||
return "sequel_background_source"
|
||||
if any(k in t for k in _SECONDARY_TITLE_MARKERS):
|
||||
return "secondary_context_source"
|
||||
title_tokens = _title_query_tokens(t.replace("_", " "))
|
||||
title_stems = {_stem_token_for_match(tok) for tok in title_tokens}
|
||||
if qtokens_stem and len(title_stems & qtokens_stem) >= max(1, len(qtokens_stem) - 1):
|
||||
return "primary_answer_source"
|
||||
return "background_source"
|
||||
|
||||
|
||||
def _search_titles(conn, qtokens: list[str], limit: int) -> list[tuple]:
|
||||
|
|
@ -1576,19 +1507,7 @@ def _rerank(
|
|||
# list-pages and franchise/sequel siblings even when the list-page wins
|
||||
# on body-density. Tuned against the JP-dinosaurs and "where is florida"
|
||||
# defects.
|
||||
SOURCE_ROLE_RANK_WEIGHTS = {
|
||||
"primary_answer_source": 2.0,
|
||||
"secondary_context_source": 0.7,
|
||||
"background_source": 0.9,
|
||||
"noisy_background_source": 0.3,
|
||||
"sequel_background_source": 0.3,
|
||||
"unclassified": 1.0,
|
||||
# Self-reference: STRICT live providence records past kindergarten
|
||||
# window. Treated like background — Wikipedia stays canonical
|
||||
# primary; self-reference is supplementary anchoring trusted as
|
||||
# fact unless the verifier falsifies the underlying record.
|
||||
"self_reference_source": 0.9,
|
||||
}
|
||||
from arborist.qa.source_roles import SOURCE_ROLE_RANK_WEIGHTS # noqa: E402
|
||||
|
||||
|
||||
def _rerank_by_source_role(hits: list[_Hit], question: str) -> list[_Hit]:
|
||||
|
|
|
|||
113
arborist/qa/source_roles.py
Normal file
113
arborist/qa/source_roles.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"""Source-role classifier + per-role weighting (Phase 1 of legacy
|
||||
``query.py`` collapse, ticket #53).
|
||||
|
||||
Pure-function module: takes a document title + question-stem set and
|
||||
returns one of seven role tokens used by the retrieval orchestrator
|
||||
to (a) bias rank scores and (b) split the per-source context-char
|
||||
budget. Same logic the legacy ``_classify_source_role`` shipped — this
|
||||
file is the un-import-cycled home so ``run_query`` and ``query``
|
||||
share one implementation.
|
||||
|
||||
Imports ``_title_query_tokens`` + ``_stem_token_for_match`` from
|
||||
``arborist.qa.query`` for now; those helpers will move to
|
||||
``arborist.qa._text_norm`` in a later Phase 1 step.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
# Per-role multiplier when allocating the per-source slice of the
|
||||
# total context budget. Primary answer source claims ~2× the slice;
|
||||
# noisy/sequel pages claim ~0.5×. Running ``char_budget`` deduction
|
||||
# in the orchestrator still bounds total context — weights just shift
|
||||
# how the budget gets divided.
|
||||
SOURCE_ROLE_BUDGET_WEIGHTS = {
|
||||
"primary_answer_source": 2.0,
|
||||
"secondary_context_source": 1.0,
|
||||
"noisy_background_source": 0.5,
|
||||
"sequel_background_source": 0.5,
|
||||
"background_source": 1.0,
|
||||
"unclassified": 1.0,
|
||||
# STRICT-live providence records past the kindergarten window.
|
||||
# Wikipedia stays canonical primary; self-reference is supplementary
|
||||
# anchoring trusted as fact unless the verifier falsifies it.
|
||||
"self_reference_source": 1.0,
|
||||
}
|
||||
|
||||
# Per-role score multiplier applied during the rerank step.
|
||||
# Primary gets +2.0× promotion; noisy/sequel get demoted to 0.3×.
|
||||
# Mismatch with BUDGET_WEIGHTS is intentional — rank order and budget
|
||||
# share are different policy levers.
|
||||
SOURCE_ROLE_RANK_WEIGHTS = {
|
||||
"primary_answer_source": 2.0,
|
||||
"secondary_context_source": 0.7,
|
||||
"background_source": 0.9,
|
||||
"noisy_background_source": 0.3,
|
||||
"sequel_background_source": 0.3,
|
||||
"unclassified": 1.0,
|
||||
"self_reference_source": 0.9,
|
||||
}
|
||||
|
||||
# Title patterns that demote a source's role. Lower-cased substring.
|
||||
# 2026-04-30: extended to catch tie-in spinoff titles (the JP-dinosaurs
|
||||
# query lazy-anchored on "Jurassic Park: Operation Genesis" — a video
|
||||
# game whose enumerative dinosaur tables pattern-matched the question
|
||||
# shape more cleanly than the actual film article's prose).
|
||||
_NOISY_TITLE_MARKERS = (
|
||||
"score", "music", "soundtrack", "video game", "video games",
|
||||
"merchandise", "discography", "operation genesis", "the game",
|
||||
)
|
||||
_SEQUEL_TITLE_MARKERS = (
|
||||
" iii", " ii)", " ii ", " iv", " v ", " v)", "lost world", "sequel",
|
||||
" 2)", " 3)", " 4)",
|
||||
)
|
||||
_SECONDARY_TITLE_MARKERS = (
|
||||
"list of", "characters", "franchise", "history of", "people",
|
||||
"timeline of",
|
||||
)
|
||||
|
||||
|
||||
def classify_source_role(
|
||||
title: str | None,
|
||||
qtokens_stem: set[str],
|
||||
*,
|
||||
document_uri: str | None = None,
|
||||
) -> str:
|
||||
"""Tag a source by its likely role for an N-token query.
|
||||
|
||||
URI-scheme classification fires first: documents whose URI starts
|
||||
with ``arborist://providence/`` are self-promoted providence
|
||||
records (per ``arborist/sources/providence.py``) and classify as
|
||||
``self_reference_source`` regardless of title shape — that role
|
||||
captures the trust model "STRICT-as-fact unless verifier
|
||||
falsifies."
|
||||
|
||||
Order matters for the title-based fallback: noisy/sequel/secondary
|
||||
markers fire first because they catch peripheral pages whose
|
||||
titles otherwise overlap query tokens fully (e.g. ``Jurassic Park
|
||||
(film score)`` shares 3 stems with ``{dinosaur, jurassic, park,
|
||||
film}`` but is not the primary answer source for a dinosaurs
|
||||
question). Primary requires the strongest title coverage (N-1
|
||||
of N stems present).
|
||||
"""
|
||||
if document_uri and document_uri.startswith("arborist://providence/"):
|
||||
return "self_reference_source"
|
||||
if not title:
|
||||
return "unclassified"
|
||||
t = title.lower()
|
||||
if any(k in t for k in _NOISY_TITLE_MARKERS):
|
||||
return "noisy_background_source"
|
||||
if any(k in t for k in _SEQUEL_TITLE_MARKERS):
|
||||
return "sequel_background_source"
|
||||
if any(k in t for k in _SECONDARY_TITLE_MARKERS):
|
||||
return "secondary_context_source"
|
||||
# Lazy import — `_title_query_tokens` + `_stem_token_for_match`
|
||||
# live in query.py today; they move to _text_norm.py in a later
|
||||
# Phase 1 step. Import-cycle-safe by deferring to call time.
|
||||
from arborist.qa.query import _title_query_tokens, _stem_token_for_match
|
||||
title_tokens = _title_query_tokens(t.replace("_", " "))
|
||||
title_stems = {_stem_token_for_match(tok) for tok in title_tokens}
|
||||
if qtokens_stem and len(title_stems & qtokens_stem) >= max(
|
||||
1, len(qtokens_stem) - 1
|
||||
):
|
||||
return "primary_answer_source"
|
||||
return "background_source"
|
||||
Loading…
Add table
Add a link
Reference in a new issue