Skip to content

Commit a7a9fe4

Browse files
Coding-Dev-Toolslatency-c5
andauthored
feat(recall): add ENGRAPHIS_RECALL_NARROW_ARM opt-in for lower-latency recall (#191)
Co-authored-by: latency-c5 <c5@local>
1 parent 585d670 commit a7a9fe4

2 files changed

Lines changed: 221 additions & 18 deletions

File tree

engraphis/core/recall.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,18 @@ def __init__(self, store: Store, embedder, vector_index, reranker: Optional[Rera
195195
max(1, int(resolved_cap)) if resolved_cap is not None else None
196196
)
197197
self._planner_slot = threading.BoundedSemaphore(1)
198+
# Latency knob: an operator may opt in to a narrower prompt-only first
199+
# arm for small-k callers (k <= 20) where the B2 P2 latency tier
200+
# widened the search by candidate_k + min(250, candidate_k*3). Setting
201+
# ``ENGRAPHIS_RECALL_NARROW_ARM`` to any non-empty, non-zero value
202+
# switches the formula to ``min(50, candidate_k * 2)`` which is ~2x
203+
# less work at k=8 (16 vs 32). This is a recall-quality trade-off:
204+
# the default wider arm remains untouched unless the operator opts in.
205+
# The narrow arm is gated on k <= 20 because the regression that
206+
# motivated the wider arm only showed up on small-k callers, and the
207+
# larger-k callers need the full widening to find approved evidence.
208+
narrow_raw = os.environ.get("ENGRAPHIS_RECALL_NARROW_ARM", "").strip()
209+
self._narrow_arm_opt_in = bool(narrow_raw) and narrow_raw not in ("0", "false", "no")
198210
# "ppr" (default) = Personalized PageRank over entities+links (multi-hop);
199211
# "1hop" = the Phase-1 entity expansion, kept for fallback and ablation.
200212
self.graph_mode = graph_mode
@@ -315,28 +327,24 @@ def recall(self, query: str, flt: Optional[SearchFilter] = None, *, k: int = 8,
315327
candidate_ceiling = candidate_k
316328
arm_candidate_k = candidate_k
317329
if prompt_only:
318-
arm_candidate_k = candidate_k + min(250, candidate_k * 3)
319-
# Latency knob (see __init__ and ``ARM_CANDIDATE_K_DEFAULT``).
320-
# The effective cap clamps the widened first page so the common
321-
# case stays cheap; the escalation ceiling below is intentionally
322-
# NOT clamped by it. Escalation only fires when the first page came
323-
# back saturated yet short on prompt-eligible records (the
324-
# untrusted-heavy vault), so the extra scan is paid only when the
325-
# trusted evidence would otherwise be unreachable. The ceiling
326-
# itself stays bounded by PROMPT_ONLY_MAX_CANDIDATES.
327-
# Clamp the widened first arm to the effective cap, but never
328-
# below the caller's requested candidate_k so a small scope
329-
# still searches at least as deep as requested.
330+
narrow_arm_active = bool(
331+
self._narrow_arm_opt_in and max(1, int(k)) <= 20
332+
)
333+
if narrow_arm_active:
334+
arm_candidate_k = max(max(1, int(k)), min(50, candidate_k * 2))
335+
ceiling_bound = min(
336+
PROMPT_ONLY_MAX_CANDIDATES, candidate_k * 4
337+
)
338+
else:
339+
arm_candidate_k = candidate_k + min(250, candidate_k * 3)
340+
ceiling_bound = min(
341+
PROMPT_ONLY_MAX_CANDIDATES,
342+
max(PROMPT_ONLY_MIN_CANDIDATES, candidate_k * 16),
343+
)
330344
arm_candidate_k = max(
331345
candidate_k, min(arm_cap, arm_candidate_k)
332346
)
333-
ceiling_bound = min(
334-
PROMPT_ONLY_MAX_CANDIDATES,
335-
max(PROMPT_ONLY_MIN_CANDIDATES, candidate_k * 16),
336-
)
337347
if self._arm_candidate_k_cap is not None:
338-
# Explicit operator override: every index query, including the
339-
# escalation pass, respects it.
340348
ceiling_bound = min(ceiling_bound, self._arm_candidate_k_cap)
341349
candidate_ceiling = max(arm_candidate_k, ceiling_bound)
342350
run_configs = [
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"""Tests for the opt-in ``ENGRAPHIS_RECALL_NARROW_ARM`` latency knob.
2+
3+
B2's 4th-pass review found the PR #171 prompt-only widening
4+
(``candidate_k + min(250, candidate_k*3)``) costs ~5x more matrix-vector
5+
work on a 49-fact corpus at k=8 (32ms -> 175ms). That regression was the
6+
user-accepted trade-off for keeping recall quality on small-k callers,
7+
but operators who care more about latency than top-of-list precision can
8+
opt in to a narrower arm by setting ``ENGRAPHIS_RECALL_NARROW_ARM=1``.
9+
10+
When the knob is enabled, the first prompt-only arm is clamped to
11+
``max(k, min(50, candidate_k*2))`` and the escalation ceiling is clamped
12+
to ``min(PROMPT_ONLY_MAX_CANDIDATES, candidate_k*4)`` so the second page
13+
does not silently undo the savings. The narrow arm is gated on k <= 20
14+
because larger-k callers still need the full widening to find approved
15+
evidence.
16+
17+
Default behavior (env var unset / 0 / empty) is unchanged — the wider
18+
arm from PR #171 is preserved verbatim.
19+
20+
These tests prove both halves of the contract: the env var is honored
21+
when set, and the wider arm is the default when it is not.
22+
"""
23+
from __future__ import annotations
24+
25+
from engraphis.backends import DeterministicEmbedder
26+
from engraphis.backends.reranker import IdentityReranker
27+
from engraphis.core.interfaces import MemoryRecord, SearchFilter
28+
from engraphis.core.recall import RecallEngine
29+
from engraphis.core.store import Store
30+
31+
32+
class _SemanticTestEmbedder(DeterministicEmbedder):
33+
supports_semantic_search = True
34+
embedding_mode = "semantic"
35+
36+
37+
class _RecordingIndex:
38+
"""Vector-index double that records every arm size it was queried with."""
39+
40+
def __init__(self):
41+
self.requested: list[int] = []
42+
43+
def search(self, query, k, *, filter=None):
44+
self.requested.append(int(k))
45+
return [(f"mem_{i}", float(k - i)) for i in range(min(k, 4))]
46+
47+
48+
def _add(store, emb, wid, rid, text, **kw):
49+
provenance = dict(kw.get("provenance") or {
50+
"source": "test", "trusted": True, "review_state": "approved",
51+
})
52+
if provenance.get("trusted") is True:
53+
provenance.setdefault("review_state", "approved")
54+
kw["provenance"] = provenance
55+
return store.add_memory(MemoryRecord(
56+
id="", content=text, workspace_id=wid, repo_id=rid,
57+
embedding=emb.embed([text])[0], **kw,
58+
))
59+
60+
61+
def test_narrow_arm_opt_in_default_is_false(monkeypatch):
62+
"""Without the env var the opt-in flag is False — the wider arm stays the
63+
default for every caller, including small-k ones."""
64+
monkeypatch.delenv("ENGRAPHIS_RECALL_NARROW_ARM", raising=False)
65+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
66+
_RecordingIndex(), IdentityReranker())
67+
assert eng._narrow_arm_opt_in is False
68+
69+
70+
def test_narrow_arm_opt_in_reads_env_var(monkeypatch):
71+
"""Any non-empty, non-zero env value flips the opt-in flag; bad/zero
72+
values are ignored so a typo cannot silently change retrieval behavior."""
73+
monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "1")
74+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
75+
_RecordingIndex(), IdentityReranker())
76+
assert eng._narrow_arm_opt_in is True
77+
78+
monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "true")
79+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
80+
_RecordingIndex(), IdentityReranker())
81+
assert eng._narrow_arm_opt_in is True
82+
83+
monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "0")
84+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
85+
_RecordingIndex(), IdentityReranker())
86+
assert eng._narrow_arm_opt_in is False
87+
88+
monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "false")
89+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
90+
_RecordingIndex(), IdentityReranker())
91+
assert eng._narrow_arm_opt_in is False
92+
93+
monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", " ")
94+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
95+
_RecordingIndex(), IdentityReranker())
96+
assert eng._narrow_arm_opt_in is False
97+
98+
99+
def test_narrow_arm_opt_in_changes_first_arm_at_k_8(monkeypatch):
100+
"""With the env var set, the first prompt-only arm at k=8 shrinks from
101+
8 + min(250, 24) = 32 to min(50, 8*2) = 16. This is the latency win
102+
the knob is meant to unlock."""
103+
monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "1")
104+
index = _RecordingIndex()
105+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
106+
index, IdentityReranker())
107+
store = eng.store
108+
wid = store.get_or_create_workspace("w")
109+
for i in range(60):
110+
_add(store, eng.embedder, wid, None, f"fact {i}")
111+
112+
result = eng.recall("fact 5", SearchFilter(workspace_id=wid), k=8,
113+
candidate_k=8, prompt_only=True)
114+
115+
assert index.requested[0] == 16 # 8 + 24 (default) would have been 32
116+
assert result.candidate_k_used == 16
117+
# The result must still be non-empty: the narrow arm must not crash
118+
# recall on a trusted-only corpus.
119+
assert result.count >= 1
120+
121+
122+
def test_narrow_arm_does_not_change_default(monkeypatch):
123+
"""Without the env var the wider arm is preserved. This is the
124+
user-accepted trade-off: 5x latency for top-of-list recall."""
125+
monkeypatch.delenv("ENGRAPHIS_RECALL_NARROW_ARM", raising=False)
126+
index = _RecordingIndex()
127+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
128+
index, IdentityReranker())
129+
store = eng.store
130+
wid = store.get_or_create_workspace("w")
131+
for i in range(60):
132+
_add(store, eng.embedder, wid, None, f"fact {i}")
133+
134+
result = eng.recall("fact 5", SearchFilter(workspace_id=wid), k=8,
135+
candidate_k=8, prompt_only=True)
136+
137+
# PR #171 default: 8 + min(250, 8*3) = 8 + 24 = 32
138+
assert index.requested[0] == 32
139+
assert result.candidate_k_used == 32
140+
141+
142+
def test_narrow_arm_only_applies_to_small_k(monkeypatch):
143+
"""The narrow arm is gated on k <= 20. A k=50 caller must still see
144+
the wider arm even with the env var set — that is the caller class
145+
that motivated the widening in the first place."""
146+
monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "1")
147+
index = _RecordingIndex()
148+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
149+
index, IdentityReranker())
150+
store = eng.store
151+
wid = store.get_or_create_workspace("w")
152+
for i in range(60):
153+
_add(store, eng.embedder, wid, None, f"fact {i}")
154+
155+
eng.recall("fact 5", SearchFilter(workspace_id=wid), k=50,
156+
candidate_k=50, prompt_only=True)
157+
158+
# k=50 is above the gate; wider arm preserved: 50 + min(250, 150) = 200.
159+
assert index.requested[0] == 200
160+
161+
# k=20 is at the boundary and should still be narrow: 20*2 = 40.
162+
index2 = _RecordingIndex()
163+
eng2 = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
164+
index2, IdentityReranker())
165+
eng2.recall("fact 5", SearchFilter(workspace_id=wid), k=20,
166+
candidate_k=20, prompt_only=True)
167+
assert index2.requested[0] == 40
168+
169+
# k=21 is just past the gate; wider arm preserved: 21 + min(250, 63) = 84.
170+
index3 = _RecordingIndex()
171+
eng3 = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
172+
index3, IdentityReranker())
173+
eng3.recall("fact 5", SearchFilter(workspace_id=wid), k=21,
174+
candidate_k=21, prompt_only=True)
175+
assert index3.requested[0] == 84
176+
177+
178+
def test_narrow_arm_does_not_apply_to_non_prompt_only(monkeypatch):
179+
"""The narrow arm is a prompt-only knob. A non-prompt recall with the
180+
env var set must behave exactly like the default, because the wider
181+
widening lives inside the ``if prompt_only`` block."""
182+
monkeypatch.setenv("ENGRAPHIS_RECALL_NARROW_ARM", "1")
183+
index = _RecordingIndex()
184+
eng = RecallEngine(Store(":memory:"), _SemanticTestEmbedder(256),
185+
index, IdentityReranker())
186+
store = eng.store
187+
wid = store.get_or_create_workspace("w")
188+
for i in range(60):
189+
_add(store, eng.embedder, wid, None, f"fact {i}")
190+
191+
# include_untrusted=True forces prompt_only=False regardless of the
192+
# env var; the first arm must be the raw candidate_k with no widening.
193+
eng.recall("fact 5", SearchFilter(workspace_id=wid), k=8,
194+
candidate_k=8, prompt_only=False, include_untrusted=True)
195+
assert index.requested[0] == 8

0 commit comments

Comments
 (0)