fix(s31): close 1 review BLOCKER — per-call retriever + run_portfolio forwards semantic_retrieval

This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 12:37:15 +02:00
commit fc69285f2c
3 changed files with 231 additions and 57 deletions

View file

@ -264,20 +264,30 @@ class VerdictStore:
that want a fresh store straight from a folder."""
return cls(verdicts=load_verdicts_from_dir(directory))
def retrieve(self, query: ProposalFeatures, k: int) -> list[Verdict]:
def retrieve(
self,
query: ProposalFeatures,
k: int,
*,
retriever: semretrieval.Retriever | None = None,
) -> list[Verdict]:
"""Return the top-``k`` verdicts. Deterministic: ties break by verdict id, so ordering
is stable across runs.
Ranking is delegated to ``self.retriever``, defaulting to ``StructuralRetriever`` the
same weighted structural score and ``(-similarity, id)`` key as before the seam existed.
A caller that installs a ``HybridRanker`` opts into an additional semantic term."""
Ranking resolves in three steps: the explicit ``retriever`` argument, then
``self.retriever``, then ``StructuralRetriever`` the same weighted structural score and
``(-similarity, id)`` key as before the seam existed.
``retriever`` is PER CALL and keyword-only. It exists because the alternative assigning
``store.retriever`` mutates an object the caller owns, so an opt-in made for one
retrieval silently governed every later use of that store (including a subsequent run with
the flag OFF). ``self.retriever`` survives as the store-level default for callers that
genuinely want a store to rank one way for its whole lifetime; see ``docs/extending.md``."""
if k <= 0:
raise ValueError(f"k must be positive, got {k}")
ranker = (
self.retriever
if self.retriever is not None
else semretrieval.StructuralRetriever(similarity)
)
ranker = retriever if retriever is not None else self.retriever
if ranker is None:
ranker = semretrieval.StructuralRetriever(similarity)
return ranker.rank(query, self.verdicts, k)
def add(self, verdict: Verdict) -> None:
@ -298,14 +308,24 @@ class ExpeLContextProvider(ContextProvider):
"""Wraps ``VerdictStore.retrieve`` for ExpeL few-shot injection via the GA
``ContextProvider`` hook."""
def __init__(self, store: VerdictStore, query: ProposalFeatures, *, k: int = 3) -> None:
def __init__(
self,
store: VerdictStore,
query: ProposalFeatures,
*,
k: int = 3,
retriever: semretrieval.Retriever | None = None,
) -> None:
super().__init__(source_id="expel-verdictstore")
self._store = store
self._query = query
self._k = k
# Per-call ranker, threaded straight through to ``retrieve`` — see its docstring for why
# this is a parameter rather than an assignment on the store.
self._retriever = retriever
def format_fewshot(self) -> str:
hits = self._store.retrieve(self._query, self._k)
hits = self._store.retrieve(self._query, self._k, retriever=self._retriever)
body = "\n".join(f"- [{v.id}] {v.decision}: {v.rationale}" for v in hits)
return f"Relevant prior verdicts (ExpeL few-shot):\n{body}"