mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
fix(rerank): harden web_search rerank fallback and correct stale docs
list()-materialize the reranker's output inside the guarded block so a None / non-iterable / lazily-raising reranker falls back to native order instead of raising out of web_search, and reject bool indices (an int subclass) the same way _parse_hits already does. Drop leftover web_fetch references from the rerank settings and Reranker role help (reranking is wired into web_search only), and document both endpoint paths (tools.reranker_alias and tools.rerank_url).
This commit is contained in:
@@ -359,6 +359,25 @@ class TestWebSearchReranking:
|
||||
out = _format_searxng(_results("A", "B"), "q", reranker=boom)
|
||||
assert out.index("[A]") < out.index("[B]") # native order preserved
|
||||
|
||||
def test_none_order_falls_back_to_native_order(self):
|
||||
# A reranker that returns None (or any non-iterable) must fall back, not
|
||||
# raise — list() materializes it inside the guarded block.
|
||||
out = _format_searxng(_results("A", "B"), "q", reranker=lambda q, d: None)
|
||||
assert out.index("[A]") < out.index("[B]") # native order preserved
|
||||
|
||||
def test_non_int_indices_ignored(self):
|
||||
# bool is an int subclass; True/False must not be honored as indices 1/0.
|
||||
# Junk entries are skipped while valid ints still apply: only 2 and 0 here.
|
||||
out = _format_searxng(
|
||||
_results("A", "B", "C"),
|
||||
"q",
|
||||
max_results=3,
|
||||
reranker=lambda q, d: [True, "1", 2, 0],
|
||||
)
|
||||
# 2 -> C, 0 -> A; B (no valid index) is kept after. If True were honored
|
||||
# as index 1, B would jump to the front — this pins it last.
|
||||
assert out.index("[C]") < out.index("[A]") < out.index("[B]")
|
||||
|
||||
def test_skipped_for_single_result(self):
|
||||
called = {"n": 0}
|
||||
|
||||
|
||||
@@ -5205,7 +5205,7 @@ const MODEL_ROLES = [
|
||||
{
|
||||
label: "Reranker",
|
||||
description:
|
||||
"Reranks web_search results and selects the most relevant chunks of large web_fetch pages. Point at a model whose base_url is a Cohere/Jina-compatible /rerank endpoint and whose capabilities include supports_rerank. Empty falls back to the tools.rerank_url setting (if set).",
|
||||
"Reranks web_search results. Point at a model whose base_url is a Cohere/Jina-compatible /rerank endpoint and whose capabilities include supports_rerank. Empty falls back to the tools.rerank_url setting (if set).",
|
||||
aliasKey: "tools.reranker_alias",
|
||||
fallbackKind: "disabled",
|
||||
mediaCapability: "supports_rerank",
|
||||
|
||||
@@ -1359,7 +1359,7 @@ class ChatSession:
|
||||
)
|
||||
|
||||
def _rerank_enabled_for(self, tool: str) -> bool:
|
||||
"""Whether reranking is enabled for ``tool`` ('web_search' | 'web_fetch').
|
||||
"""Whether reranking is enabled for ``tool`` (currently: 'web_search').
|
||||
|
||||
The per-tool toggles default on; the operative gate is whether an
|
||||
endpoint is configured (``_resolve_rerank_client`` returns None when
|
||||
|
||||
@@ -259,9 +259,9 @@ def _build_registry() -> dict[str, SettingDef]:
|
||||
help="Full URL of a Cohere/Jina-compatible rerank endpoint, including the path "
|
||||
"(e.g. 'http://vllm:8000/rerank', 'http://tei:8080/rerank', or "
|
||||
"'https://api.cohere.com/v2/rerank'). Turnstone runs no rerank model itself: "
|
||||
"web_search results and large web_fetch pages are reranked by POSTing to this "
|
||||
"endpoint. Empty disables reranking entirely. Overrides config.toml [tools] "
|
||||
"rerank_url and $TURNSTONE_RERANK_URL.",
|
||||
"web_search results are reranked by POSTing to this endpoint. Empty disables "
|
||||
"reranking entirely. Overrides config.toml [tools] rerank_url and "
|
||||
"$TURNSTONE_RERANK_URL.",
|
||||
),
|
||||
SettingDef(
|
||||
"tools.rerank_model",
|
||||
@@ -291,9 +291,10 @@ def _build_registry() -> dict[str, SettingDef]:
|
||||
True,
|
||||
"Rerank web_search results when a rerank endpoint is configured",
|
||||
"tools",
|
||||
help="When a rerank endpoint is set (tools.rerank_url), re-order web_search "
|
||||
"results by query relevance before returning the top hits. No effect when no "
|
||||
"endpoint is configured. Disable to keep the search backend's native ranking.",
|
||||
help="When a rerank endpoint is configured (via tools.reranker_alias or "
|
||||
"tools.rerank_url), re-order web_search results by query relevance before "
|
||||
"returning the top hits. No effect when no endpoint is configured. Disable to "
|
||||
"keep the search backend's native ranking.",
|
||||
),
|
||||
SettingDef(
|
||||
"tools.reranker_alias",
|
||||
|
||||
@@ -173,14 +173,22 @@ def _rerank_results(
|
||||
tail = results[_RERANK_POOL:]
|
||||
try:
|
||||
docs = [f"{r.get('title', '')}\n{r.get('content') or ''}".strip() for r in pool]
|
||||
order = reranker(query, docs)
|
||||
# Materialize inside the try so a None / non-iterable / lazily-raising
|
||||
# reranker falls back here instead of exploding the splice loop below.
|
||||
order = list(reranker(query, docs))
|
||||
except Exception as e:
|
||||
log.warning("rerank failed; using native result order: %s", e)
|
||||
return results
|
||||
seen: set[int] = set()
|
||||
reordered: list[dict[str, Any]] = []
|
||||
for idx in order:
|
||||
if isinstance(idx, int) and 0 <= idx < len(pool) and idx not in seen:
|
||||
# bool is an int subclass — reject a stray True/False posing as index 1/0.
|
||||
if (
|
||||
isinstance(idx, int)
|
||||
and not isinstance(idx, bool)
|
||||
and 0 <= idx < len(pool)
|
||||
and idx not in seen
|
||||
):
|
||||
seen.add(idx)
|
||||
reordered.append(pool[idx])
|
||||
if not reordered:
|
||||
|
||||
Reference in New Issue
Block a user