mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-15 09:11:24 +00:00
Align tag contracts and index health behavior
This commit is contained in:
parent
c1c339e396
commit
7a7890c940
7 changed files with 52 additions and 12 deletions
|
|
@ -12,7 +12,13 @@ from ...schema import FileFrontMatter, FileNode
|
|||
class LocalTagIndex(BaseTagIndex):
|
||||
"""Maintain bidirectional path/tag relationships without separate source I/O."""
|
||||
|
||||
reserved_tag_keys = frozenset(FileFrontMatter.model_fields)
|
||||
reserved_tag_keys = frozenset(FileFrontMatter.model_fields) | {
|
||||
"kind",
|
||||
"session_id",
|
||||
"source_conversation",
|
||||
"source_resource",
|
||||
"status",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -47,7 +53,7 @@ class LocalTagIndex(BaseTagIndex):
|
|||
for item in value:
|
||||
if isinstance(item, bool) or not isinstance(item, (str, int)):
|
||||
continue
|
||||
raw = " ".join(str(item).split())
|
||||
raw = "_".join(str(item).split())
|
||||
if not raw or len(raw) > self.max_tag_length:
|
||||
continue
|
||||
if not any(char.isalnum() for char in raw):
|
||||
|
|
@ -183,9 +189,9 @@ class LocalTagIndex(BaseTagIndex):
|
|||
raise ValueError("order must be one of ['asc', 'desc']")
|
||||
|
||||
async with self._maintenance_lock:
|
||||
items: list[TagListItem] = (
|
||||
[(tag, len(paths)) for tag, paths in self.tag_to_paths.items()] if self.is_healthy else []
|
||||
)
|
||||
if not self.is_healthy:
|
||||
raise RuntimeError("tag index is unavailable")
|
||||
items: list[TagListItem] = [(tag, len(paths)) for tag, paths in self.tag_to_paths.items()]
|
||||
|
||||
if order_by == "tag":
|
||||
items.sort(key=lambda item: item[0], reverse=order == "desc")
|
||||
|
|
|
|||
|
|
@ -207,6 +207,8 @@ jobs:
|
|||
- session_id
|
||||
steps:
|
||||
- backend: auto_memory_cc_step
|
||||
- backend: auto_tag_step
|
||||
max_tags_per_file: 3
|
||||
|
||||
auto_resource:
|
||||
backend: base
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ def normalize_memory_tags(
|
|||
for item in value:
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
tag = " ".join(item.split())
|
||||
tag = "_".join(item.split())
|
||||
if not tag or len(tag) > max_tag_length or not any(char.isalnum() for char in tag):
|
||||
continue
|
||||
canonical = tag.casefold()
|
||||
|
|
@ -181,6 +181,11 @@ class AutoTagStep(BaseStep):
|
|||
return self.context.response
|
||||
|
||||
tag_index = self.file_store.require_tag_index() if targets else None
|
||||
if tag_index is not None and not tag_index.is_healthy:
|
||||
self.context.response.success = False
|
||||
if initial_success:
|
||||
self.context.response.answer = "Error: tag index unavailable"
|
||||
return self.context.response
|
||||
max_tag_length = self._index_limit(tag_index, "max_tag_length", _DEFAULT_MAX_MEMORY_TAG_LENGTH)
|
||||
index_max_tags = self._index_limit(tag_index, "max_tags_per_file", None)
|
||||
if index_max_tags is not None and self.max_tags_per_file > index_max_tags:
|
||||
|
|
|
|||
|
|
@ -91,6 +91,16 @@ async def test_auto_tag_handles_noop_and_rejects_invalid_preconditions(tmp_path,
|
|||
assert not wrapper.calls
|
||||
assert note.read_bytes() == before
|
||||
|
||||
indexed_store.tag_index = LocalTagIndex()
|
||||
indexed_store.tag_index.set_healthy(False)
|
||||
response = await AutoTagStep(file_store=indexed_store, agent_wrapper=wrapper)(
|
||||
RuntimeContext(changes=[{"change": "added", "path": "daily/2026-09-09/note.md"}]),
|
||||
)
|
||||
assert response.success is False
|
||||
assert response.answer == "Error: tag index unavailable"
|
||||
assert not wrapper.calls
|
||||
assert note.read_bytes() == before
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_tag_filters_paths_and_continues_after_one_file_fails(tmp_path, monkeypatch):
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ def test_auto_tag_normalizes_frontmatter_tags():
|
|||
"宁德时代",
|
||||
"黄金",
|
||||
],
|
||||
) == ["OpenAI", "Sam Altman", "宁德时代"]
|
||||
) == ["OpenAI", "Sam_Altman", "宁德时代"]
|
||||
# pylint: disable=use-implicit-booleaness-not-comparison
|
||||
assert normalize_memory_tags(None) == []
|
||||
assert normalize_memory_tags("OpenAI") == []
|
||||
|
|
|
|||
|
|
@ -263,6 +263,10 @@ def test_configs_define_original_jobs_without_daily_variants():
|
|||
{"backend": "auto_memory_step"},
|
||||
{"backend": "auto_tag_step", "max_tags_per_file": 3},
|
||||
]
|
||||
assert default["jobs"]["auto_memory_cc"]["steps"] == [
|
||||
{"backend": "auto_memory_cc_step"},
|
||||
{"backend": "auto_tag_step", "max_tags_per_file": 3},
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ def test_tag_normalization_and_bidirectional_mutations() -> None:
|
|||
async def run() -> None:
|
||||
index = LocalTagIndex(max_tags_per_file=3)
|
||||
await index.start()
|
||||
await index.upsert_nodes([_node("daily/a.md", ["Python", "PYTHON", "C++", ".NET", "ignored"])])
|
||||
await index.upsert_nodes([_node("daily/a.md", ["Python Tag", "PYTHON TAG", "C++", ".NET", "ignored"])])
|
||||
|
||||
assert await index.tags_for_path("daily/a.md") == ["python", "c++", ".net"]
|
||||
assert await index.paths_for_tags(["PYTHON"]) == ["daily/a.md"]
|
||||
assert await index.tags_for_path("daily/a.md") == ["python_tag", "c++", ".net"]
|
||||
assert await index.paths_for_tags(["PYTHON TAG"]) == ["daily/a.md"]
|
||||
assert index.tag_to_paths == {
|
||||
"python": {"daily/a.md"},
|
||||
"python_tag": {"daily/a.md"},
|
||||
"c++": {"daily/a.md"},
|
||||
".net": {"daily/a.md"},
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ def test_queries_are_not_truncated_by_per_file_tag_limit() -> None:
|
|||
"daily/a.md",
|
||||
"daily/c.md",
|
||||
]
|
||||
assert index.normalize_query_tags([" A ", "B", "c", "a", "too-long-tag", " "]) == ["a", "b", "c"]
|
||||
assert index.normalize_query_tags([" A ", "B", "c c", "a", "too-long-tag", " "]) == ["a", "b", "c_c"]
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
|
@ -180,6 +180,10 @@ def test_list_tags_paginates_and_applies_default_sort_orders() -> None:
|
|||
with pytest.raises(ValueError, match=message):
|
||||
await index.list_tags(**kwargs)
|
||||
|
||||
index.set_healthy(False)
|
||||
with pytest.raises(RuntimeError, match="tag index is unavailable"):
|
||||
await index.list_tags()
|
||||
|
||||
store = LocalFileStore(name="test", embedding_store="", tag_index="")
|
||||
store.tag_index = LocalTagIndex()
|
||||
await store.tag_index.rebuild([_node("daily/a.md", ["ReMe"])])
|
||||
|
|
@ -229,6 +233,11 @@ def test_configured_frontmatter_key_contract() -> None:
|
|||
(123, "tag_key must be a non-empty string", False),
|
||||
("name", "tag_key must not be a reserved frontmatter key", False),
|
||||
("description", "tag_key must not be a reserved frontmatter key", False),
|
||||
("kind", "tag_key must not be a reserved frontmatter key", False),
|
||||
("session_id", "tag_key must not be a reserved frontmatter key", False),
|
||||
("source_conversation", "tag_key must not be a reserved frontmatter key", False),
|
||||
("source_resource", "tag_key must not be a reserved frontmatter key", False),
|
||||
("status", "tag_key must not be a reserved frontmatter key", False),
|
||||
("", "tag_key must be a non-empty string", True),
|
||||
("name", "tag_key must not be a reserved frontmatter key", True),
|
||||
("description", "tag_key must not be a reserved frontmatter key", True),
|
||||
|
|
@ -508,3 +517,7 @@ def test_default_config_enables_tag_index_with_explicit_key() -> None:
|
|||
{"backend": "auto_memory_step"},
|
||||
{"backend": "auto_tag_step", "max_tags_per_file": 3},
|
||||
]
|
||||
assert config["jobs"]["auto_memory_cc"]["steps"] == [
|
||||
{"backend": "auto_memory_cc_step"},
|
||||
{"backend": "auto_tag_step", "max_tags_per_file": 3},
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue