fix(reindex): return a readable summary so Studio stops showing a placeholder

`ReindexStep` assigned the store's report dict straight to
`response.answer`. ReMe Studio's settings panel already anticipates a string
answer:

    typeof result.answer === "string" && result.answer ? result.answer : t("indexRebuilt")

(`reme_studio/app/settings-center.tsx`), so with a dict answer that branch was
unreachable and every rebuild reported the generic "index rebuilt" text. The
operator never saw which scopes ran or how many entries each indexed.

Render the report as one summary line — `Reindexed scope=all: bm25=7,
embedding=5, tag=2` for the composite scope and `Reindexed scope=bm25: 3
indexed` for a single one. The report itself still lands in metadata, so
programmatic consumers are unaffected, and Studio needs no change because its
string branch now fires.

`reindex` is the only answer-carrying consumer of this step; no test asserted
the answer content, only metadata, so nothing else moved.
This commit is contained in:
Ziyang Guo 2026-09-21 15:16:51 +08:00
parent 1f67a6ce29
commit 2427c7a3b5
2 changed files with 47 additions and 1 deletions

View file

@ -4,6 +4,24 @@ from ..base_step import BaseStep
from ...components import R
def _render_reindex(details: dict) -> str:
"""Render the store's reindex report as one readable summary line.
ReMe Studio's settings panel shows ``response.answer`` when it is a string
and otherwise falls back to a generic "index rebuilt" message
(``reme_studio/app/settings-center.tsx``). Returning the report dict as the
answer left that branch unreachable, so the operator never learned what was
actually rebuilt. The report itself stays in ``metadata`` for programmatic
consumers.
"""
scope = str(details.get("scope") or "all")
per_scope = {name: entry for name, entry in details.items() if isinstance(entry, dict)}
if per_scope:
counts = ", ".join(f"{name}={entry.get('indexed', 0)}" for name, entry in per_scope.items())
return f"Reindexed scope={scope}: {counts}"
return f"Reindexed scope={scope}: {details.get('indexed', 0)} indexed"
@R.register("reindex_step")
class ReindexStep(BaseStep):
"""Rebuild BM25, embeddings, and/or tags without scanning workspace files."""
@ -13,7 +31,7 @@ class ReindexStep(BaseStep):
scope = str(self.context.get("scope", "all"))
details = await self.file_store.reindex(scope)
self.context.response.answer = details
self.context.response.answer = _render_reindex(details)
self.context.response.metadata.update(details)
self.context.response.metadata["scope"] = scope
return self.context.response

View file

@ -23,6 +23,7 @@ async def test_reindex_step_delegates_scope(scope):
store.reindex.assert_awaited_once_with(scope)
store.clear.assert_not_called()
assert response.metadata == {"indexed": 3, "scope": scope}
assert response.answer == f"Reindexed scope={scope}: 3 indexed"
@pytest.mark.asyncio
@ -41,6 +42,33 @@ async def test_reindex_step_delegates_all_once():
store.reindex.assert_awaited_once_with("all")
assert response.metadata == details
assert response.answer == "Reindexed scope=all: bm25=3, embedding=3, tag=3"
@pytest.mark.asyncio
async def test_reindex_answer_is_a_readable_summary_of_the_report():
"""The answer is a string, so Studio shows it instead of a generic message.
ReMe Studio only displays ``response.answer`` when it is a string
(``reme_studio/app/settings-center.tsx``); a dict answer silently fell back
to the generic "index rebuilt" text and the operator never saw the counts.
The full report stays in metadata either way.
"""
store = LocalFileStore(name="test_reindex_answer", embedding_store="")
store.reindex = AsyncMock(
return_value={
"scope": "all",
"bm25": {"scope": "bm25", "indexed": 7},
"embedding": {"scope": "embedding", "indexed": 5},
"tag": {"scope": "tag", "indexed": 2},
},
)
response = await ReindexStep(file_store=store)(RuntimeContext(scope="all"))
assert isinstance(response.answer, str)
assert response.answer == "Reindexed scope=all: bm25=7, embedding=5, tag=2"
assert response.metadata["bm25"] == {"scope": "bm25", "indexed": 7}
def test_reindex_job_schema_exposes_tag_scope():