mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-15 09:11:24 +00:00
Extract automatic tagging into a dedicated step
This commit is contained in:
parent
261614b550
commit
5ac7d679fe
11 changed files with 387 additions and 259 deletions
|
|
@ -188,7 +188,7 @@ jobs:
|
|||
- messages
|
||||
steps:
|
||||
- backend: auto_memory_step
|
||||
enable_tags: true
|
||||
- backend: auto_tag_step
|
||||
|
||||
auto_memory_cc:
|
||||
backend: base
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from ._evolve import now, passthrough_response
|
||||
from .auto_image_resource import AutoImageResourceStep
|
||||
from .auto_memory import AutoMemoryStep
|
||||
from .auto_tag import AutoTagStep
|
||||
from .auto_memory_cc import AutoMemoryCCStep
|
||||
from .auto_resource import AutoResourceStep
|
||||
from .auto_text_resource import AutoTextResourceStep
|
||||
|
|
@ -22,6 +23,7 @@ __all__ = [
|
|||
"AutoImageResourceStep",
|
||||
"passthrough_response",
|
||||
"AutoMemoryStep",
|
||||
"AutoTagStep",
|
||||
"AutoMemoryCCStep",
|
||||
"AutoResourceStep",
|
||||
"AutoTextResourceStep",
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ from ...components import R
|
|||
|
||||
_SESSION_ID_KEY = "session_id"
|
||||
_SOURCE_CONVERSATION_KEY = "source_conversation"
|
||||
_MAX_TAGS = 8
|
||||
_MAX_TAG_LENGTH = 64
|
||||
_MESSAGE_TIME_ALIASES = ("time_created", "timestamp", "createdAt", "timeCreated", "created_time")
|
||||
|
||||
|
||||
|
|
@ -61,39 +59,6 @@ def _normalize_msg_timestamp(item: dict) -> dict:
|
|||
return item
|
||||
|
||||
|
||||
def _normalize_tags(value) -> list[str]:
|
||||
"""Return up to eight unique, retrieval-friendly frontmatter tags.
|
||||
|
||||
Tags are stored uniformly as strings. Technical tokens such as ``GPT-5``,
|
||||
``C++``, ``C#``, and ``.NET`` are valid; whitespace-delimited phrases and
|
||||
punctuation-only values are not.
|
||||
"""
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
|
||||
tags: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in value:
|
||||
if isinstance(item, bool) or not isinstance(item, (str, int)):
|
||||
continue
|
||||
tag = str(item).strip()
|
||||
if not tag or len(tag) > _MAX_TAG_LENGTH:
|
||||
continue
|
||||
if any(char.isspace() for char in tag):
|
||||
continue
|
||||
if not any(char.isalnum() for char in tag):
|
||||
continue
|
||||
|
||||
dedupe_key = tag.casefold()
|
||||
if dedupe_key in seen:
|
||||
continue
|
||||
seen.add(dedupe_key)
|
||||
tags.append(tag)
|
||||
if len(tags) >= _MAX_TAGS:
|
||||
break
|
||||
return tags
|
||||
|
||||
|
||||
@R.register("auto_memory_step")
|
||||
class AutoMemoryStep(BaseStep):
|
||||
"""Record conversation facts into a daily note via an Agent."""
|
||||
|
|
@ -102,7 +67,6 @@ class AutoMemoryStep(BaseStep):
|
|||
super().__init__(**kwargs)
|
||||
self.create_tools: list[str] = ["daily_write"]
|
||||
self.update_tools: list[str] = ["read", "edit", "frontmatter_update", "write"]
|
||||
self._missing_tag_index_warned = False
|
||||
|
||||
def _session_dir(self) -> str:
|
||||
return normalize_posix_path(str(self.config_value("session_dir")))
|
||||
|
|
@ -119,29 +83,6 @@ class AutoMemoryStep(BaseStep):
|
|||
def _daily_note_path(self, day: str, name: str) -> str:
|
||||
return f"{self.config_value('daily_dir')}/{day}/{name}.md"
|
||||
|
||||
def _tags_key(self) -> str | None:
|
||||
"""Return the configured tag frontmatter key when tag generation is usable."""
|
||||
if not self.kwargs.get("enable_tags", False):
|
||||
return None
|
||||
try:
|
||||
tag_index = self.file_store.tag_index
|
||||
key = tag_index.tag_key if tag_index is not None else None
|
||||
except (AttributeError, KeyError, RuntimeError):
|
||||
key = None
|
||||
if key:
|
||||
return key
|
||||
if not self._missing_tag_index_warned:
|
||||
self.logger.warning(
|
||||
f"[{self.name}] enable_tags is true but no tag_index is configured on file_store; "
|
||||
"tag generation is disabled",
|
||||
)
|
||||
self._missing_tag_index_warned = True
|
||||
return None
|
||||
|
||||
def _tags_enabled(self) -> bool:
|
||||
"""Whether this step should generate and normalize frontmatter tags."""
|
||||
return self._tags_key() is not None
|
||||
|
||||
def _frontmatter(self, path: str) -> dict:
|
||||
post = frontmatter.loads((self.file_store.workspace_path / path).read_text(encoding="utf-8"))
|
||||
return dict(post.metadata or {})
|
||||
|
|
@ -183,9 +124,6 @@ class AutoMemoryStep(BaseStep):
|
|||
_SESSION_ID_KEY: session_id,
|
||||
_SOURCE_CONVERSATION_KEY: self._session_link(session_id),
|
||||
}
|
||||
tags_key = self._tags_key()
|
||||
if tags_key is not None:
|
||||
metadata[tags_key] = _normalize_tags(current.get(tags_key))
|
||||
if all(current.get(key) == value for key, value in metadata.items()):
|
||||
return
|
||||
response = await self.run_job(
|
||||
|
|
@ -338,6 +276,7 @@ class AutoMemoryStep(BaseStep):
|
|||
# pylint: disable=too-many-return-statements
|
||||
async def execute(self):
|
||||
assert self.context is not None
|
||||
self.context["modified_paths"] = []
|
||||
raw_messages = self.context.get("messages") or []
|
||||
session_id: str = self.context.get("session_id", "")
|
||||
memory_hint: str = self.context.get("memory_hint", "")
|
||||
|
|
@ -399,12 +338,8 @@ class AutoMemoryStep(BaseStep):
|
|||
f"created={created} msgs={len(messages)} hint={bool(memory_hint)}",
|
||||
)
|
||||
template_key = "user_message_create" if created else "user_message_update"
|
||||
tags_key = self._tags_key()
|
||||
enable_tags = tags_key is not None
|
||||
user_message = self.prompt_format(
|
||||
template_key,
|
||||
enable_tags=enable_tags,
|
||||
tags_key=tags_key or "tags",
|
||||
today=day,
|
||||
note=memory_hint or "(none)",
|
||||
note_path=note_path,
|
||||
|
|
@ -422,11 +357,7 @@ class AutoMemoryStep(BaseStep):
|
|||
reply_kwargs["injected_job_kwargs"] = {"_allowed_paths": [note_path]}
|
||||
result = await self.agent_wrapper.reply(
|
||||
user_message,
|
||||
system_prompt=self.prompt_format(
|
||||
"system_prompt",
|
||||
enable_tags=enable_tags,
|
||||
tags_key=tags_key or "tags",
|
||||
),
|
||||
system_prompt=self.prompt_format("system_prompt"),
|
||||
job_tools=self.create_tools if created else self.update_tools,
|
||||
**reply_kwargs,
|
||||
)
|
||||
|
|
@ -453,11 +384,14 @@ class AutoMemoryStep(BaseStep):
|
|||
return
|
||||
note_path = str(note["path"])
|
||||
try:
|
||||
if not created or enable_tags:
|
||||
if not created:
|
||||
await self._ensure_memory_frontmatter(note_path, session_id)
|
||||
if not created:
|
||||
note_path = await self._rename_from_frontmatter_name(note_path, day)
|
||||
except RuntimeError as exc:
|
||||
modified = self._note_modified(before_note_path, before_note_bytes, note_path)
|
||||
if modified and note_path:
|
||||
self.context["modified_paths"] = [note_path]
|
||||
self.context.response.success = False
|
||||
self.context.response.answer = str(exc)
|
||||
self.context.response.metadata.update(
|
||||
|
|
@ -465,7 +399,7 @@ class AutoMemoryStep(BaseStep):
|
|||
"date": day,
|
||||
"path": note_path,
|
||||
"created": created,
|
||||
"modified": self._note_modified(before_note_path, before_note_bytes, note_path),
|
||||
"modified": modified,
|
||||
"n_messages": len(messages),
|
||||
},
|
||||
)
|
||||
|
|
@ -473,6 +407,8 @@ class AutoMemoryStep(BaseStep):
|
|||
return
|
||||
|
||||
modified = self._note_modified(before_note_path, before_note_bytes, note_path)
|
||||
if modified:
|
||||
self.context["modified_paths"] = [note_path]
|
||||
daily_dir = self.config_value("daily_dir")
|
||||
self.logger.info(f"[{self.name}] refresh index start date={day} daily_dir={daily_dir}")
|
||||
index_payload = await refresh_day_index(self.file_store, day, daily_dir)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ system_prompt: |
|
|||
|
||||
- `name` = a concise, stable topic/event filename stem, such as `cold-remedies` or `project-kickoff-decision`. Do not include today's date or the daily directory date; the outer daily path already records the date. For existing notes, update it when a better filename is clearly warranted.
|
||||
- `description` = a thorough summary; vague descriptions like "notes" / "misc" are unacceptable.
|
||||
[enable_tags] - `{tags_key}` = 0–8 unique retrieval keywords for the complete note. Always include the field, using `[]` when no useful tags exist. Each tag must be one string with no whitespace, may contain technical punctuation (for example `GPT-5`, `C++`, `C#`, or `.NET`), must contain at least one letter or digit, and must be at most 64 characters. Store numeric tags as strings, for example `"100"`.
|
||||
- **Never set `status`** — it is a field reserved for downstream processing.
|
||||
system_prompt_zh: |
|
||||
你是自动记忆系统。你的职责是将最近对话中的核心信息记录到日记记忆中。思考人类会从这段对话中自然地记住什么——不是所有内容,而是真正重要的信息。
|
||||
|
|
@ -44,7 +43,6 @@ system_prompt_zh: |
|
|||
|
||||
- `name` = 简洁、稳定的主题/事件文件名 stem,例如 `cold-remedies` 或 `project-kickoff-decision`。不要包含今天日期或日记目录日期;外层日记路径已经记录日期。对已有笔记,如果明显有更好的文件名,就更新它。
|
||||
- `description` = 详细总结;模糊的描述如 "notes" / "misc" 不可接受。
|
||||
[enable_tags] - `{tags_key}` = 针对完整笔记的 0–8 个不重复检索关键词。该字段必须始终存在;没有合适关键词时使用 `[]`。每个 tag 必须是一个不含空白字符的字符串,可以包含技术名称中的标点(例如 `GPT-5`、`C++`、`C#` 或 `.NET`),必须至少包含一个字母或数字,且不得超过 64 个字符。数字 tag 也保存为字符串,例如 `"100"`。
|
||||
- **永远不要设置 `status`**——它是下游处理保留的字段。
|
||||
|
||||
user_message_create: |
|
||||
|
|
@ -70,13 +68,11 @@ user_message_create: |
|
|||
|
||||
Create the note in one shot:
|
||||
`daily_write name=<name> description=<description> session_id={session_id} date={today} content=<body>`
|
||||
[enable_tags] Also pass `metadata={{"{tags_key}": [<tag>, ...]}}` in the same `daily_write` call.
|
||||
|
||||
- Generate `name` as a concise, stable topic/event filename stem for this memory. Prefer a reusable topic or event summary, optionally in kebab-case.
|
||||
- Do not include today's date or the daily directory date in `name`; the note already lives under today's daily path.
|
||||
- `name` must be a valid single filename component: no slash, backslash, leading/trailing whitespace, or characters like `< > : " | ? *`.
|
||||
- `description` must be a thorough summary of the body — specific enough that the description alone conveys all key information.
|
||||
[enable_tags] - Generate 0–8 `{tags_key}` values from the complete note. Always pass `metadata={{"{tags_key}": [...]}}`, including an empty list when there are no useful tags. Follow the system prompt's tag format exactly.
|
||||
|
||||
## Step 3 — Summary
|
||||
|
||||
|
|
@ -108,13 +104,11 @@ user_message_create_zh: |
|
|||
|
||||
一次性创建笔记:
|
||||
`daily_write name=<name> description=<description> session_id={session_id} date={today} content=<正文>`
|
||||
[enable_tags] 在同一次 `daily_write` 调用中另外传入 `metadata={{"{tags_key}": [<tag>, ...]}}`。
|
||||
|
||||
- 由你生成 `name`,作为这条记忆简洁、稳定的主题/事件文件名 stem。优先使用可复用的主题或事件总结,可以采用 kebab-case。
|
||||
- `name` 不要包含今天日期或日记目录日期;笔记已经位于当天日记路径下。
|
||||
- `name` 必须是合法的单个文件名组件:不能包含 slash、反斜杠、首尾空白,或 `< > : " | ? *` 等字符。
|
||||
- `description` 必须是正文的详尽总结——具体到仅凭 description 就能传达全部核心信息。
|
||||
[enable_tags] - 根据完整笔记生成 0–8 个 `{tags_key}` 值。必须始终传入 `metadata={{"{tags_key}": [...]}}`,没有合适关键词时也要传入空数组。严格遵守 system prompt 的 tag 格式。
|
||||
|
||||
## 步骤 3 — 总结
|
||||
|
||||
|
|
@ -161,22 +155,18 @@ user_message_update: |
|
|||
Execution:
|
||||
1. Use `edit path={note_path} old=<original fragment> new=<replacement fragment>` for each section that needs updating. You may call `edit` multiple times.
|
||||
2. After body changes, refresh frontmatter with `frontmatter_update path={note_path} metadata={{"name": "<updated filename stem>", "description": "<updated summary>"}}`.
|
||||
[enable_tags] Also regenerate 0–8 tags from the complete merged note and include `"{tags_key}": [<tag>, ...]` in the same metadata object. Always include `{tags_key}`, using `[]` when none are useful.
|
||||
- Keep the existing `name` only when it is already the best concise topic/event filename stem. The system will rename the file after your final response.
|
||||
- Do not add today's date or the daily directory date to `name`.
|
||||
3. If `edit` fails repeatedly (e.g., cannot find the original text due to formatting mismatch), fall back to `write path={note_path} name=<name> description=<description> content=<full body>` for a complete rewrite.
|
||||
[enable_tags] In this fallback, also pass `metadata={{"{tags_key}": [<tag>, ...]}}`.
|
||||
|
||||
## Step 3b — Full Write (Empty File Fallback)
|
||||
|
||||
The file exists but its body is empty. Write the full content in one shot:
|
||||
`write path={note_path} name=<name> description=<description> content=<body>`
|
||||
[enable_tags] Also pass `metadata={{"{tags_key}": [<tag>, ...]}}` in this call.
|
||||
|
||||
- Use a concise, stable topic/event `name`; filename changes are applied after your final response.
|
||||
- Do not include today's date or the daily directory date in `name`.
|
||||
- `description` must be a thorough summary of the body — specific enough that the description alone conveys all key information.
|
||||
[enable_tags] - Generate 0–8 tags from the complete body and always include `metadata={{"{tags_key}": [...]}}`, using `[]` when none are useful.
|
||||
|
||||
## Step 4 — Summary
|
||||
|
||||
|
|
@ -224,22 +214,18 @@ user_message_update_zh: |
|
|||
执行:
|
||||
1. 对需要更新的每个部分使用 `edit path={note_path} old=<原文片段> new=<替换片段>`。可以多次调用 `edit`。
|
||||
2. 正文变更后,刷新 frontmatter:`frontmatter_update path={note_path} metadata={{"name": "<更新后的文件名 stem>", "description": "<更新后的总结>"}}`。
|
||||
[enable_tags] 同时根据合并后的完整笔记重新生成 0–8 个 tags,并在同一个 metadata 对象中包含 `"{tags_key}": [<tag>, ...]`。必须始终包含 `{tags_key}`,没有合适关键词时使用 `[]`。
|
||||
- 只有当前 `name` 已经是最合适的简洁主题/事件文件名 stem 时才保留。系统会在你最终回复后负责重命名文件。
|
||||
- 不要在 `name` 中加入今天日期或日记目录日期。
|
||||
3. 如果 `edit` 多次失败(如因格式不匹配找不到原文),退回 `write path={note_path} name=<name> description=<description> content=<完整正文>` 全量重写。
|
||||
[enable_tags] 在该 fallback 中还要传入 `metadata={{"{tags_key}": [<tag>, ...]}}`。
|
||||
|
||||
## 步骤 3b — 全量写入(空文件 fallback)
|
||||
|
||||
文件存在但正文为空。一次性写入完整内容:
|
||||
`write path={note_path} name=<name> description=<description> content=<正文>`
|
||||
[enable_tags] 在该调用中另外传入 `metadata={{"{tags_key}": [<tag>, ...]}}`。
|
||||
|
||||
- 使用简洁、稳定的主题/事件 `name`;文件名变化会在你最终回复后应用。
|
||||
- `name` 不要包含今天日期或日记目录日期。
|
||||
- `description` 必须是正文的详尽总结——具体到仅凭 description 就能传达全部核心信息。
|
||||
[enable_tags] - 根据完整正文生成 0–8 个 tags,并始终包含 `metadata={{"{tags_key}": [...]}}`;没有合适关键词时使用 `[]`。
|
||||
|
||||
## 步骤 4 — 总结
|
||||
|
||||
|
|
|
|||
148
reme/steps/evolve/auto_tag.py
Normal file
148
reme/steps/evolve/auto_tag.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"""auto_tag — generate frontmatter tags for modified Markdown files."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import frontmatter
|
||||
|
||||
from ._evolve import agent_reply_result_text
|
||||
from ..base_step import BaseStep
|
||||
from ..file_io import parse_daily_date, refresh_day_index
|
||||
from ..file_io._path import display_path, resolve_path
|
||||
from ..index import normalize_posix_path
|
||||
from ...components import R
|
||||
|
||||
_MAX_TAGS = 8
|
||||
_MAX_TAG_LENGTH = 64
|
||||
|
||||
|
||||
def normalize_tags(value) -> list[str]:
|
||||
"""Return unique retrieval-friendly tags that satisfy the storage contract."""
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
|
||||
tags: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in value:
|
||||
if isinstance(item, bool) or not isinstance(item, (str, int)):
|
||||
continue
|
||||
tag = str(item).strip()
|
||||
if not tag or len(tag) > _MAX_TAG_LENGTH:
|
||||
continue
|
||||
if any(char.isspace() for char in tag):
|
||||
continue
|
||||
if not any(char.isalnum() for char in tag):
|
||||
continue
|
||||
dedupe_key = tag.casefold()
|
||||
if dedupe_key in seen:
|
||||
continue
|
||||
seen.add(dedupe_key)
|
||||
tags.append(tag)
|
||||
if len(tags) >= _MAX_TAGS:
|
||||
break
|
||||
return tags
|
||||
|
||||
|
||||
@R.register("auto_tag_step")
|
||||
class AutoTagStep(BaseStep):
|
||||
"""Generate tags for each eligible path produced by an earlier Step."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.tools = ["read", "list_tags", "frontmatter_read", "frontmatter_update"]
|
||||
|
||||
def _eligible_paths(self) -> tuple[list[str], list[str]]:
|
||||
assert self.context is not None
|
||||
raw_paths = self.context.get("modified_paths") or []
|
||||
if not isinstance(raw_paths, list):
|
||||
raw_paths = [raw_paths]
|
||||
|
||||
workspace = Path(self.file_store.workspace_path or ".").resolve()
|
||||
eligible: list[str] = []
|
||||
ignored: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw_path in raw_paths:
|
||||
raw = str(raw_path or "").strip()
|
||||
target, err = resolve_path(workspace, raw)
|
||||
if err or target is None or not target.is_file() or target.suffix.lower() != ".md":
|
||||
ignored.append(raw)
|
||||
continue
|
||||
path = normalize_posix_path(display_path(workspace, target))
|
||||
if path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
eligible.append(path)
|
||||
return eligible, ignored
|
||||
|
||||
async def _process_path(self, path: str, tag_key: str) -> str:
|
||||
result = await self.agent_wrapper.reply(
|
||||
self.prompt_format("user_message", path=path, tags_key=tag_key),
|
||||
system_prompt=self.prompt_format("system_prompt", tags_key=tag_key),
|
||||
job_tools=self.tools,
|
||||
injected_job_kwargs={"_allowed_paths": [path]},
|
||||
)
|
||||
|
||||
target = Path(self.file_store.workspace_path or ".") / path
|
||||
metadata = dict(frontmatter.loads(target.read_text(encoding="utf-8")).metadata or {})
|
||||
normalized = normalize_tags(metadata.get(tag_key))
|
||||
if metadata.get(tag_key) != normalized:
|
||||
response = await self.run_job(
|
||||
"frontmatter_update",
|
||||
path=path,
|
||||
metadata={tag_key: normalized},
|
||||
_allowed_paths=[path],
|
||||
)
|
||||
if not response.success:
|
||||
raise RuntimeError(str(response.answer))
|
||||
return agent_reply_result_text(result)
|
||||
|
||||
def _daily_date(self, path: str) -> str | None:
|
||||
daily_dir = normalize_posix_path(str(self.config_value("daily_dir"))).strip("/")
|
||||
prefix = f"{daily_dir}/"
|
||||
if not path.startswith(prefix):
|
||||
return None
|
||||
remainder = path[len(prefix) :]
|
||||
parts = remainder.split("/")
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
return parse_daily_date(parts[0])
|
||||
|
||||
async def execute(self):
|
||||
assert self.context is not None
|
||||
initial_success = self.context.response.success
|
||||
tag_index = getattr(self.file_store, "tag_index", None)
|
||||
if tag_index is None:
|
||||
self.context.response.success = False
|
||||
self.context.response.answer = "Error: tag index is not configured"
|
||||
return self.context.response
|
||||
|
||||
eligible, ignored = self._eligible_paths()
|
||||
tagged: list[str] = []
|
||||
failed: list[dict[str, str]] = []
|
||||
summaries: list[str] = []
|
||||
dates: set[str] = set()
|
||||
for path in eligible:
|
||||
if day := self._daily_date(path):
|
||||
dates.add(day)
|
||||
try:
|
||||
summaries.append(await self._process_path(path, tag_index.tag_key))
|
||||
tagged.append(path)
|
||||
except Exception as exc: # pylint: disable=broad-exception-caught
|
||||
failed.append({"path": path, "error": str(exc)})
|
||||
self.logger.warning(f"[{self.name}] failed path={path}: {exc}")
|
||||
|
||||
indexes = [
|
||||
await refresh_day_index(self.file_store, day, self.config_value("daily_dir")) for day in sorted(dates)
|
||||
]
|
||||
self.context.response.success = initial_success and not failed
|
||||
if failed:
|
||||
self.context.response.answer = f"Tagged {len(tagged)} file(s); {len(failed)} failed"
|
||||
elif tagged:
|
||||
self.context.response.answer = f"Tagged {len(tagged)} file(s)"
|
||||
self.context.response.metadata["auto_tag"] = {
|
||||
"tagged_paths": tagged,
|
||||
"ignored_paths": ignored,
|
||||
"failed_paths": failed,
|
||||
"indexes": indexes,
|
||||
"summaries": [summary for summary in summaries if summary],
|
||||
}
|
||||
return self.context.response
|
||||
29
reme/steps/evolve/auto_tag.yaml
Normal file
29
reme/steps/evolve/auto_tag.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
system_prompt: |
|
||||
You add retrieval tags to one Markdown memory file at a time.
|
||||
|
||||
Read the complete target file and its frontmatter, then inspect existing tags with `list_tags`. Update only the
|
||||
`{tags_key}` frontmatter field by calling `frontmatter_update`. Do not change the body or any other frontmatter field.
|
||||
|
||||
Generate 0–8 unique retrieval keywords for the complete note. Always write `{tags_key}`, using `[]` when no useful
|
||||
tags exist. Each tag must be one string with no whitespace, may contain technical punctuation such as `GPT-5`, `C++`,
|
||||
`C#`, or `.NET`, must contain at least one letter or digit, and must be at most 64 characters. Store numeric tags as
|
||||
strings. Prefer an existing tag when it accurately expresses the same concept.
|
||||
system_prompt_zh: |
|
||||
你每次为一个 Markdown 记忆文件添加检索 tag。
|
||||
|
||||
阅读目标文件的完整内容和 frontmatter,然后通过 `list_tags` 查看已有 tag。只允许调用 `frontmatter_update`
|
||||
更新 frontmatter 的 `{tags_key}` 字段,不得修改正文或其他 frontmatter 字段。
|
||||
|
||||
根据完整笔记生成 0–8 个不重复检索关键词。必须始终写入 `{tags_key}`,没有合适关键词时使用 `[]`。每个 tag
|
||||
必须是一个不含空白字符的字符串,可以包含 `GPT-5`、`C++`、`C#`、`.NET` 等技术标点,必须至少包含一个
|
||||
字母或数字,且不得超过 64 个字符。数字 tag 也保存为字符串。含义相同时优先复用已有 tag。
|
||||
user_message: |
|
||||
Target path: {path}
|
||||
Tag field: {tags_key}
|
||||
|
||||
Read this file, select tags for its complete content, and update only its `{tags_key}` frontmatter field.
|
||||
user_message_zh: |
|
||||
目标路径:{path}
|
||||
Tag 字段:{tags_key}
|
||||
|
||||
阅读该文件,根据完整内容选择 tag,并且只更新 frontmatter 的 `{tags_key}` 字段。
|
||||
165
tests/unit/test_auto_tag.py
Normal file
165
tests/unit/test_auto_tag.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
"""Focused tests for the standalone automatic tagging Step."""
|
||||
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import frontmatter
|
||||
import pytest
|
||||
|
||||
from reme.components.agent_wrapper import BaseAgentWrapper
|
||||
from reme.components.file_store import LocalFileStore
|
||||
from reme.components.runtime_context import RuntimeContext
|
||||
from reme.components.tag_index import LocalTagIndex
|
||||
from reme.schema import Response
|
||||
from reme.steps.evolve.auto_tag import AutoTagStep, normalize_tags
|
||||
|
||||
|
||||
class _TaggingWrapper(BaseAgentWrapper):
|
||||
def __init__(
|
||||
self,
|
||||
workspace: Path,
|
||||
*,
|
||||
fail_name: str = "",
|
||||
tag_key: str = "tags",
|
||||
tags: list[object] | None = None,
|
||||
) -> None:
|
||||
super().__init__(name="tagger")
|
||||
self.workspace = workspace
|
||||
self.fail_name = fail_name
|
||||
self.tag_key = tag_key
|
||||
self.tags = ["ReMe", "Python"] if tags is None else tags
|
||||
self.calls: list[tuple[str, dict]] = []
|
||||
|
||||
async def reply(self, inputs, **kwargs) -> dict:
|
||||
text = str(inputs)
|
||||
self.calls.append((text, kwargs))
|
||||
path = kwargs["injected_job_kwargs"]["_allowed_paths"][0]
|
||||
if Path(path).name == self.fail_name:
|
||||
raise RuntimeError("tagging failed")
|
||||
target = self.workspace / path
|
||||
post = frontmatter.loads(target.read_text(encoding="utf-8"))
|
||||
post.metadata[self.tag_key] = self.tags
|
||||
target.write_text(frontmatter.dumps(post), encoding="utf-8")
|
||||
return {"result": f"tagged {path}", "last_message": {}}
|
||||
|
||||
|
||||
def _write_note(path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("---\nname: note\ndescription: useful note\n---\nbody\n", encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_tag_requires_tag_index_before_modifying_files(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
note = tmp_path / "daily/2026-09-09/note.md"
|
||||
_write_note(note)
|
||||
before = note.read_bytes()
|
||||
store = LocalFileStore(name="store", embedding_store="", tag_index="")
|
||||
wrapper = _TaggingWrapper(tmp_path)
|
||||
step = AutoTagStep(file_store=store, agent_wrapper=wrapper)
|
||||
|
||||
response = await step(RuntimeContext(modified_paths=["daily/2026-09-09/note.md"]))
|
||||
|
||||
assert response.success is False
|
||||
assert response.answer == "Error: tag index is not configured"
|
||||
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):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
first = tmp_path / "daily/2026-09-09/first.md"
|
||||
failed = tmp_path / "daily/2026-09-09/failed.md"
|
||||
_write_note(first)
|
||||
_write_note(failed)
|
||||
(tmp_path / "daily/2026-09-09/notes").mkdir()
|
||||
(tmp_path / "daily/2026-09-09/plain.txt").write_text("text", encoding="utf-8")
|
||||
|
||||
store = LocalFileStore(name="store", embedding_store="", tag_index="")
|
||||
store.tag_index = LocalTagIndex()
|
||||
wrapper = _TaggingWrapper(tmp_path, fail_name="failed.md")
|
||||
step = AutoTagStep(file_store=store, agent_wrapper=wrapper)
|
||||
context = RuntimeContext(
|
||||
modified_paths=[
|
||||
"daily/2026-09-09/failed.md",
|
||||
"daily/2026-09-09/notes",
|
||||
"daily/2026-09-09/plain.txt",
|
||||
"daily/2026-09-09/first.md",
|
||||
"daily/2026-09-09/first.md",
|
||||
],
|
||||
)
|
||||
|
||||
response = await step(context)
|
||||
|
||||
assert response.success is False
|
||||
assert [call[1]["injected_job_kwargs"] for call in wrapper.calls] == [
|
||||
{"_allowed_paths": ["daily/2026-09-09/failed.md"]},
|
||||
{"_allowed_paths": ["daily/2026-09-09/first.md"]},
|
||||
]
|
||||
assert all(
|
||||
call[1]["job_tools"] == ["read", "list_tags", "frontmatter_read", "frontmatter_update"]
|
||||
for call in wrapper.calls
|
||||
)
|
||||
assert frontmatter.loads(first.read_text(encoding="utf-8")).metadata["tags"] == ["ReMe", "Python"]
|
||||
assert "tags" not in frontmatter.loads(failed.read_text(encoding="utf-8")).metadata
|
||||
assert response.metadata["auto_tag"]["tagged_paths"] == ["daily/2026-09-09/first.md"]
|
||||
assert response.metadata["auto_tag"]["ignored_paths"] == [
|
||||
"daily/2026-09-09/notes",
|
||||
"daily/2026-09-09/plain.txt",
|
||||
]
|
||||
assert response.metadata["auto_tag"]["failed_paths"] == [
|
||||
{"path": "daily/2026-09-09/failed.md", "error": "tagging failed"},
|
||||
]
|
||||
assert "tags: ['ReMe', 'Python']" in (tmp_path / "daily/2026-09-09.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_tag_uses_configured_key_and_normalizes_agent_output(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
note = tmp_path / "memory/note.md"
|
||||
_write_note(note)
|
||||
store = LocalFileStore(name="store", embedding_store="", tag_index="")
|
||||
store.tag_index = LocalTagIndex(tag_key="keywords")
|
||||
wrapper = _TaggingWrapper(
|
||||
tmp_path,
|
||||
tag_key="keywords",
|
||||
tags=["ReMe", "reme", "memory system", "++", 100],
|
||||
)
|
||||
step = AutoTagStep(file_store=store, agent_wrapper=wrapper)
|
||||
|
||||
async def update_frontmatter(name, /, **kwargs):
|
||||
assert name == "frontmatter_update"
|
||||
post = frontmatter.loads(note.read_text(encoding="utf-8"))
|
||||
post.metadata.update(kwargs["metadata"])
|
||||
note.write_text(frontmatter.dumps(post), encoding="utf-8")
|
||||
return Response(answer="updated")
|
||||
|
||||
monkeypatch.setattr(step, "run_job", update_frontmatter)
|
||||
|
||||
response = await step(RuntimeContext(modified_paths=["memory/note.md"]))
|
||||
|
||||
assert response.success is True
|
||||
assert frontmatter.loads(note.read_text(encoding="utf-8")).metadata["keywords"] == ["ReMe", "100"]
|
||||
assert "Tag field: keywords" in wrapper.calls[0][0]
|
||||
assert "`keywords`" in wrapper.calls[0][1]["system_prompt"]
|
||||
|
||||
|
||||
def test_normalize_tags_enforces_storage_contract():
|
||||
assert normalize_tags(
|
||||
[
|
||||
"GPT-5",
|
||||
"C++",
|
||||
"C#",
|
||||
".NET",
|
||||
100,
|
||||
"memory system",
|
||||
"++",
|
||||
"ReMe",
|
||||
"reme",
|
||||
"tag7",
|
||||
"tag8",
|
||||
"tag9",
|
||||
],
|
||||
) == ["GPT-5", "C++", "C#", ".NET", "100", "ReMe", "tag7", "tag8"]
|
||||
|
|
@ -2012,28 +2012,29 @@ def test_auto_memory_reports_modified_for_create_and_false_for_skip():
|
|||
app_context=app_ctx,
|
||||
file_store=fs,
|
||||
agent_wrapper=wrapper,
|
||||
enable_tags=True,
|
||||
)
|
||||
resp = await step(
|
||||
RuntimeContext(
|
||||
messages=[{"name": "user", "role": "user", "content": "remember project detail"}],
|
||||
session_id="s1",
|
||||
),
|
||||
context = RuntimeContext(
|
||||
messages=[{"name": "user", "role": "user", "content": "remember project detail"}],
|
||||
session_id="s1",
|
||||
)
|
||||
resp = await step(context)
|
||||
resp = resp or step.context.response
|
||||
|
||||
assert resp.success is True
|
||||
assert resp.metadata["created"] is True
|
||||
assert resp.metadata["modified"] is True
|
||||
assert "tags: []" in (cwd / "daily" / today / "memory.md").read_text(encoding="utf-8")
|
||||
assert context["modified_paths"] == [f"daily/{today}/memory.md"]
|
||||
assert "tags:" not in (cwd / "daily" / today / "memory.md").read_text(encoding="utf-8")
|
||||
|
||||
wrapper.on_reply = None
|
||||
resp = await step(RuntimeContext(messages=[], session_id="s2"))
|
||||
context = RuntimeContext(messages=[], session_id="s2")
|
||||
resp = await step(context)
|
||||
resp = resp or step.context.response
|
||||
|
||||
assert resp.success is True
|
||||
assert resp.metadata["modified"] is False
|
||||
assert resp.metadata["n_messages"] == 0
|
||||
assert context["modified_paths"] == []
|
||||
finally:
|
||||
await fs.close()
|
||||
print("✓ test_auto_memory_reports_modified_for_create_and_false_for_skip passed")
|
||||
|
|
@ -2041,104 +2042,6 @@ def test_auto_memory_reports_modified_for_create_and_false_for_skip():
|
|||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_auto_memory_normalizes_tags_after_existing_note_update():
|
||||
"""Existing notes receive a refreshed, normalized tags field capped at eight entries."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmpdir, temp_chdir(tmpdir):
|
||||
cwd = Path.cwd()
|
||||
app_ctx = _make_app_context(cwd)
|
||||
fs = LocalFileStore(name="test_store", embedding_store="", tag_index="default")
|
||||
wrapper = _FakeAgentWrapper()
|
||||
await fs.start()
|
||||
_install_file_jobs(app_ctx, fs)
|
||||
try:
|
||||
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
note_path = cwd / "daily" / today / "memory.md"
|
||||
write_file(
|
||||
note_path,
|
||||
"---\nname: memory\nsession_id: s1\n"
|
||||
"source_conversation: '[[session/dialog/s1.jsonl]]'\n"
|
||||
"tags: [old]\n---\nold body\n",
|
||||
)
|
||||
|
||||
wrapper.on_reply = lambda *_: write_file(
|
||||
note_path,
|
||||
"---\nname: memory\nsession_id: s1\n"
|
||||
"source_conversation: '[[session/dialog/s1.jsonl]]'\n"
|
||||
"tags: [GPT-5, C++, C#, .NET, 100, 'memory system', '++', ReMe, reme, tag7, tag8, tag9]\n"
|
||||
"---\nupdated body\n",
|
||||
)
|
||||
|
||||
step = AutoMemoryStep(
|
||||
app_context=app_ctx,
|
||||
file_store=fs,
|
||||
agent_wrapper=wrapper,
|
||||
enable_tags=True,
|
||||
)
|
||||
resp = await step(
|
||||
RuntimeContext(
|
||||
messages=[{"name": "user", "role": "user", "content": "updated project detail"}],
|
||||
session_id="s1",
|
||||
),
|
||||
)
|
||||
resp = resp or step.context.response
|
||||
|
||||
assert resp.success is True
|
||||
metadata = step._frontmatter(f"daily/{today}/memory.md")
|
||||
assert metadata["tags"] == ["GPT-5", "C++", "C#", ".NET", "100", "ReMe", "tag7", "tag8"]
|
||||
finally:
|
||||
await fs.close()
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_auto_memory_uses_tag_index_frontmatter_key():
|
||||
"""Generate and normalize metadata under the key selected by the bound tag index."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmpdir, temp_chdir(tmpdir):
|
||||
cwd = Path.cwd()
|
||||
app_ctx = _make_app_context(cwd)
|
||||
fs = LocalFileStore(name="test_store", embedding_store="", tag_index="default")
|
||||
wrapper = _FakeAgentWrapper()
|
||||
await fs.start()
|
||||
fs.tag_index.tag_key = "keywords"
|
||||
_install_file_jobs(app_ctx, fs)
|
||||
try:
|
||||
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
note_path = cwd / "daily" / today / "memory.md"
|
||||
wrapper.on_reply = lambda *_: write_file(
|
||||
note_path,
|
||||
"---\nname: memory\nsession_id: s1\n"
|
||||
"source_conversation: '[[session/dialog/s1.jsonl]]'\n"
|
||||
"keywords: [ReMe]\n---\nbody\n",
|
||||
)
|
||||
|
||||
step = AutoMemoryStep(
|
||||
app_context=app_ctx,
|
||||
file_store=fs,
|
||||
agent_wrapper=wrapper,
|
||||
enable_tags=True,
|
||||
)
|
||||
resp = await step(
|
||||
RuntimeContext(
|
||||
messages=[{"name": "user", "role": "user", "content": "remember ReMe"}],
|
||||
session_id="s1",
|
||||
),
|
||||
)
|
||||
resp = resp or step.context.response
|
||||
|
||||
assert resp.success is True
|
||||
assert step._frontmatter(f"daily/{today}/memory.md")["keywords"] == ["ReMe"]
|
||||
assert 'metadata={"keywords"' in wrapper.inputs
|
||||
assert "`keywords`" in wrapper.kwargs["system_prompt"]
|
||||
finally:
|
||||
await fs.close()
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_auto_memory_uses_message_day_for_historical_create():
|
||||
"""AutoMemoryStep creates historical daily notes from message timestamps."""
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ from agentscope.message import Msg
|
|||
import pytest
|
||||
|
||||
from reme.steps.evolve._evolve import agent_reply_result_text, format_history
|
||||
from reme.steps.evolve.auto_memory import AutoMemoryStep, _normalize_tags, _sanitize_msg_for_save
|
||||
from reme.steps.evolve.auto_memory import AutoMemoryStep, _sanitize_msg_for_save
|
||||
from reme.steps.evolve.auto_tag import normalize_tags
|
||||
|
||||
|
||||
def test_agent_reply_result_text_uses_last_text_block():
|
||||
|
|
@ -75,9 +76,9 @@ def test_sanitize_msg_for_save_drops_tool_results_and_base64_data():
|
|||
assert sanitized.content[1].name == "memory_search"
|
||||
|
||||
|
||||
def test_auto_memory_normalizes_frontmatter_tags():
|
||||
def test_auto_tag_normalizes_frontmatter_tags():
|
||||
"""Tags keep technical punctuation, reject phrases, de-duplicate, and stop at eight."""
|
||||
assert _normalize_tags(
|
||||
assert normalize_tags(
|
||||
[
|
||||
"GPT-5",
|
||||
"C++",
|
||||
|
|
@ -95,10 +96,10 @@ def test_auto_memory_normalizes_frontmatter_tags():
|
|||
],
|
||||
) == ["GPT-5", "C++", "C#", ".NET", "100", "ReMe", "tag7", "tag8"]
|
||||
# pylint: disable=use-implicit-booleaness-not-comparison
|
||||
assert _normalize_tags(None) == []
|
||||
assert _normalize_tags("GPT-5") == []
|
||||
assert normalize_tags(None) == []
|
||||
assert normalize_tags("GPT-5") == []
|
||||
# pylint: enable=use-implicit-booleaness-not-comparison
|
||||
assert _normalize_tags(["x" * 65, True, {}, "valid"]) == ["valid"]
|
||||
assert normalize_tags(["x" * 65, True, {}, "valid"]) == ["valid"]
|
||||
|
||||
|
||||
def test_auto_memory_accepts_message_timestamp_aliases():
|
||||
|
|
|
|||
|
|
@ -8,16 +8,12 @@ the injected keys from the tool schema exposed to the model.
|
|||
# pylint: disable=protected-access,missing-function-docstring
|
||||
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from reme.components.agent_wrapper import AsAgentWrapper, BaseAgentWrapper, CcAgentWrapper
|
||||
from reme.components.file_store import LocalFileStore
|
||||
from reme.components.tag_index import LocalTagIndex
|
||||
from reme.schema import Response
|
||||
from reme.steps.evolve.auto_memory import AutoMemoryStep
|
||||
from reme.steps.evolve.auto_memory_cc import AutoMemoryCCStep
|
||||
|
||||
|
||||
class _Job:
|
||||
|
|
@ -240,25 +236,7 @@ def test_auto_memory_keeps_original_tool_names():
|
|||
assert step.update_tools == ["read", "edit", "frontmatter_update", "write"]
|
||||
|
||||
|
||||
def test_auto_memory_tags_are_disabled_by_default_and_enabled_through_kwargs():
|
||||
assert AutoMemoryStep()._tags_enabled() is False
|
||||
assert AutoMemoryCCStep()._tags_enabled() is False
|
||||
store = LocalFileStore(embedding_store="", tag_index="")
|
||||
store.tag_index = LocalTagIndex(tag_key="keywords")
|
||||
assert AutoMemoryStep(enable_tags=True, file_store=store)._tags_enabled() is True
|
||||
assert AutoMemoryStep(enable_tags=True)._tags_enabled() is False
|
||||
|
||||
|
||||
def test_auto_memory_warns_once_when_tags_enabled_without_tag_index():
|
||||
step = AutoMemoryStep(enable_tags=True)
|
||||
step.logger = MagicMock()
|
||||
|
||||
assert step._tags_enabled() is False
|
||||
assert step._tags_enabled() is False
|
||||
step.logger.warning.assert_called_once()
|
||||
|
||||
|
||||
def test_auto_memory_tags_prompt_follows_step_kwarg():
|
||||
def test_auto_memory_prompt_has_no_tag_responsibility():
|
||||
step = AutoMemoryStep()
|
||||
prompt_kwargs = {
|
||||
"today": "2026-09-01",
|
||||
|
|
@ -267,42 +245,16 @@ def test_auto_memory_tags_prompt_follows_step_kwarg():
|
|||
"history": "user: remember GPT-5",
|
||||
}
|
||||
|
||||
disabled_system = step.prompt_format("system_prompt", enable_tags=step._tags_enabled())
|
||||
disabled_create = step.prompt_format("user_message_create", enable_tags=step._tags_enabled(), **prompt_kwargs)
|
||||
disabled_update = step.prompt_format(
|
||||
system = step.prompt_format("system_prompt")
|
||||
create = step.prompt_format("user_message_create", **prompt_kwargs)
|
||||
update = step.prompt_format(
|
||||
"user_message_update",
|
||||
enable_tags=step._tags_enabled(),
|
||||
note_path="daily/2026-09-01/memory.md",
|
||||
**prompt_kwargs,
|
||||
)
|
||||
assert "`tags`" not in disabled_system
|
||||
assert 'metadata={"tags"' not in disabled_create
|
||||
assert '"tags"' not in disabled_update
|
||||
|
||||
store = LocalFileStore(embedding_store="", tag_index="")
|
||||
store.tag_index = LocalTagIndex(tag_key="keywords")
|
||||
enabled_step = AutoMemoryStep(enable_tags=True, file_store=store)
|
||||
enabled_system = enabled_step.prompt_format(
|
||||
"system_prompt",
|
||||
enable_tags=enabled_step._tags_enabled(),
|
||||
tags_key=enabled_step._tags_key(),
|
||||
)
|
||||
enabled_create = enabled_step.prompt_format(
|
||||
"user_message_create",
|
||||
enable_tags=enabled_step._tags_enabled(),
|
||||
tags_key=enabled_step._tags_key(),
|
||||
**prompt_kwargs,
|
||||
)
|
||||
enabled_update = enabled_step.prompt_format(
|
||||
"user_message_update",
|
||||
enable_tags=enabled_step._tags_enabled(),
|
||||
tags_key=enabled_step._tags_key(),
|
||||
note_path="daily/2026-09-01/memory.md",
|
||||
**prompt_kwargs,
|
||||
)
|
||||
assert "`keywords`" in enabled_system
|
||||
assert 'metadata={"keywords"' in enabled_create
|
||||
assert '"keywords"' in enabled_update
|
||||
assert "tag" not in system.casefold()
|
||||
assert "tag" not in create.casefold()
|
||||
assert "tag" not in update.casefold()
|
||||
|
||||
|
||||
def test_auto_memory_create_prompts_match_upstream_date_arguments():
|
||||
|
|
@ -312,8 +264,8 @@ def test_auto_memory_create_prompts_match_upstream_date_arguments():
|
|||
prompt_file = Path("reme/steps/evolve/auto_memory.yaml")
|
||||
evolve_prompt = prompt_file.read_text(encoding="utf-8")
|
||||
assert "date={today}" in evolve_prompt or "`date`: {today}" in evolve_prompt or "`date`:{today}" in evolve_prompt
|
||||
assert '"{tags_key}": [<tag>, ...]' in evolve_prompt
|
||||
assert '"{tags_key}": []' in evolve_prompt or "using `[]`" in evolve_prompt
|
||||
assert "enable_tags" not in evolve_prompt
|
||||
assert "tags_key" not in evolve_prompt
|
||||
|
||||
|
||||
def test_configs_define_original_jobs_without_daily_variants():
|
||||
|
|
@ -328,7 +280,10 @@ def test_configs_define_original_jobs_without_daily_variants():
|
|||
assert name not in jobs, f"{config_name} unexpectedly defines {name}"
|
||||
|
||||
default = resolve_app_config(config="default", log_config=False)
|
||||
assert default["jobs"]["auto_memory"]["steps"][0]["enable_tags"] is True
|
||||
assert default["jobs"]["auto_memory"]["steps"] == [
|
||||
{"backend": "auto_memory_step"},
|
||||
{"backend": "auto_tag_step"},
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -539,4 +539,7 @@ def test_default_config_enables_tag_index_with_explicit_key() -> None:
|
|||
assert config["components"]["tag_index"]["default"]["tag_key"] == "tags"
|
||||
assert config["components"]["file_store"]["default"]["tag_index"] == "default"
|
||||
assert config["jobs"]["search"]["parameters"]["properties"]["tags"]["default"] == []
|
||||
assert config["jobs"]["auto_memory"]["steps"][0]["enable_tags"] is True
|
||||
assert config["jobs"]["auto_memory"]["steps"] == [
|
||||
{"backend": "auto_memory_step"},
|
||||
{"backend": "auto_tag_step"},
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue