feat(s31): Retriever seam — StructuralRetriever default + HybridRanker with injected similarity

This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 06:16:23 +02:00
commit ae684c89d3
3 changed files with 218 additions and 9 deletions

View file

@ -26,6 +26,7 @@ from __future__ import annotations
import hashlib
import os
from collections.abc import Callable, Sequence
from typing import TYPE_CHECKING, Protocol, runtime_checkable
# Pin BLAS threads BEFORE numpy is imported — OpenBLAS reads these at import time, and a
@ -39,7 +40,10 @@ os.environ.setdefault("MKL_NUM_THREADS", "1")
import numpy as np # noqa: E402 — must follow the thread pins above; see module docstring
if TYPE_CHECKING: # verdicts imports agent_framework — keep it out of the runtime import graph
from portfolio_optimiser.verdicts import ProposalFeatures
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict
# The structural score, passed in rather than imported — see module docstring.
SimilarityFn = Callable[[ProposalFeatures, ProposalFeatures], float]
# Dimension of the fake embedding. Small enough that a 500+ verdict base is a trivial matmul,
# large enough that distinct features do not collide.
@ -124,3 +128,62 @@ def cosine(a: np.ndarray, b: np.ndarray) -> float:
if norm_a == 0.0 or norm_b == 0.0:
return 0.0
return float(np.dot(a, b) / (norm_a * norm_b))
@runtime_checkable
class Retriever(Protocol):
"""Ranks candidate verdicts against a query and returns the top ``k``.
``VerdictStore.retrieve`` delegates to one of these. The store's default is
``StructuralRetriever``, which reproduces the pre-seam ordering exactly, so installing a
retriever is the ONLY way retrieval behaviour changes."""
def rank(
self, query: ProposalFeatures, candidates: Sequence[Verdict], k: int
) -> list[Verdict]: ...
class StructuralRetriever:
"""Today's ranking, behind the seam: weighted structural similarity, ties broken by ``id``.
``similarity`` is INJECTED rather than imported importing it would pull ``verdicts`` (and
thus ``agent_framework``) into this module's runtime graph. This class is the store's wired
default, not a parallel implementation kept for reference."""
def __init__(self, similarity: SimilarityFn) -> None:
self._similarity = similarity
def rank(self, query: ProposalFeatures, candidates: Sequence[Verdict], k: int) -> list[Verdict]:
return sorted(
candidates,
key=lambda v: (-self._similarity(query, v.proposal_features), v.id),
)[:k]
class HybridRanker:
"""Blends semantic cosine with the structural score: ``w * cosine + (1 - w) * structural``.
Both terms live in ``[0, 1]`` (the embedder yields non-negative unit vectors), so ``weight``
means what it reads as. Ordering uses the TOTAL order ``(-round(score, 9), id)``: rounding
absorbs any last-bit float noise, and ``id`` makes the result independent of input order even
when two candidates score identically."""
def __init__(
self,
embedder: Embedder,
similarity: SimilarityFn,
weight: float = SEMANTIC_WEIGHT_DEFAULT,
) -> None:
self._embedder = embedder
self._similarity = similarity
self._weight = weight
def rank(self, query: ProposalFeatures, candidates: Sequence[Verdict], k: int) -> list[Verdict]:
query_vector = self._embedder(query)
def score(verdict: Verdict) -> float:
semantic = cosine(query_vector, self._embedder(verdict.proposal_features))
structural = self._similarity(query, verdict.proposal_features)
return self._weight * semantic + (1.0 - self._weight) * structural
return sorted(candidates, key=lambda v: (-round(score(v), 9), v.id))[:k]

View file

@ -28,7 +28,7 @@ from typing import Any
from agent_framework import ContextProvider, SessionContext
from portfolio_optimiser import okf
from portfolio_optimiser import okf, semretrieval
_log = logging.getLogger(__name__)
@ -251,6 +251,11 @@ class VerdictStore:
verdicts: list[Verdict]
# S3.1 opt-in seam: ``None`` means the structural default, which is byte-identical to the
# pre-seam inline sort. Only an explicit caller (``run.py --semantic-retrieval``) installs a
# different ranker, so default retrieval behaviour is unchanged.
retriever: semretrieval.Retriever | None = None
@classmethod
def from_dir(cls, directory: str) -> VerdictStore:
"""Build a store from an async inbox folder (convenience constructor). ``run_project`` uses
@ -260,15 +265,20 @@ class VerdictStore:
return cls(verdicts=load_verdicts_from_dir(directory))
def retrieve(self, query: ProposalFeatures, k: int) -> list[Verdict]:
"""Return the top-``k`` verdicts by structural similarity. Deterministic: ties break
by verdict id, so ordering is stable across runs."""
"""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."""
if k <= 0:
raise ValueError(f"k must be positive, got {k}")
ranked = sorted(
self.verdicts,
key=lambda v: (-similarity(query, v.proposal_features), v.id),
ranker = (
self.retriever
if self.retriever is not None
else semretrieval.StructuralRetriever(similarity)
)
return ranked[:k]
return ranker.rank(query, self.verdicts, k)
def add(self, verdict: Verdict) -> None:
"""Persist a captured verdict in-memory.