Skip to content

Python context layer: lazy BM25 retrieval and prewarm - #241

Draft
jat255 wants to merge 2 commits into
jat255/context-layer-constructionfrom
jat255/context-layer-retrieval
Draft

Python context layer: lazy BM25 retrieval and prewarm#241
jat255 wants to merge 2 commits into
jat255/context-layer-constructionfrom
jat255/context-layer-retrieval

Conversation

@jat255

@jat255 jat255 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Second of two PRs for M4, the Python context layer (kata g1bf and hmh8). Stacked on #240, which adds the class this builds on. Python only.

ContextLayer.search(query, top_k=3) retrieves over a raghilda DuckDBStore with embed=None and a BM25 index. The index is built on first search rather than at construction, because store setup is the most expensive part of building an agent and many conversations never search. prewarm() builds it ahead of time for callers that know a search is coming.

Two constraints of the retrieval library drove the implementation, and both are covered by tests that fail without the corresponding code.

retrieve_bm25 pads its result up to top_k with unscored rows instead of dropping them, so a query matching nothing still returns a full result set. search() keeps only rows with a non-null bm25 metric. Without that filter the agent receives arbitrary chunks presented as relevant. ragnar_retrieve_bm25 returns zero rows in the same situation, so this is a difference between the engines rather than between the packages.

ingest() upserts on a document origin that it also requires to be non-empty, so each document gets a distinct synthetic origin. Sharing one collapses two files with identical text into a single chunk.

search() and prewarm() are synchronous. The milestone scope floated putting the build behind asyncio.to_thread; the thread hop belongs at the tool boundary in M5, which knows whether it is on an event loop, rather than inside the layer where it would force every caller async for a call that is a no-op on a warm store.

Nothing here is added to tests/shared/. Retrieval ranking is engine-specific and is not a cross-language contract.

The index is built on first search rather than at construction: it is the
most expensive part of building an agent, and many conversations never search.

Two constraints of the retrieval library shape this.

retrieve_bm25 pads its result up to top_k with unscored rows rather than
dropping them, so a query matching nothing still returns a full result set.
Without the metric filter, search() would hand the agent arbitrary context and
present it as relevant.

ingest() upserts on a document origin it also requires to be non-empty, so
each document gets a distinct synthetic origin. Sharing one would make two
files with identical text collapse into a single chunk.
Builds the index ahead of the first search, for callers that know a search is
coming and would rather not pay for it mid-conversation.

Kept synchronous. The milestone scope floated putting the build behind
asyncio.to_thread, but the thread hop belongs at the tool boundary, which
knows whether it is on an event loop. Inside the layer it would force every
caller async for a call that is a no-op on a warm store.
@jat255 jat255 changed the title jat255/context layer retrieval Python context layer: lazy BM25 retrieval and prewarm Sep 2, 2026
@jat255
jat255 marked this pull request as draft September 2, 2026 00:04
@jat255 jat255 added the py Affects the Python implementation label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py Affects the Python implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant