From 2427c7a3b56c2668057e30c1bff3165700957454 Mon Sep 17 00:00:00 2001 From: Ziyang Guo <121015044+RerankerGuo@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:16:51 +0800 Subject: [PATCH] fix(reindex): return a readable summary so Studio stops showing a placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- reme/steps/index/reindex.py | 20 +++++++++++++++++++- tests/unit/test_reindex.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/reme/steps/index/reindex.py b/reme/steps/index/reindex.py index 6ba44655..acbc9d83 100644 --- a/reme/steps/index/reindex.py +++ b/reme/steps/index/reindex.py @@ -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 diff --git a/tests/unit/test_reindex.py b/tests/unit/test_reindex.py index 64bd1a43..b0a350ed 100644 --- a/tests/unit/test_reindex.py +++ b/tests/unit/test_reindex.py @@ -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():