mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-06 02:48:22 +00:00
* Add optional tag generation and normalization to auto memory * Add tag index components and clean up temporary JSONL files * Preserve tag index state when reconciliation fails * Refactor and streamline application implementation * Fix pylint C1803 warnings in tag normalization tests * Document optional tag index configuration * Make tag index failures non-blocking and disable auto-memory tags * Add configurable tag indexing and tag listing * Add tag-filtered hybrid search with exact candidate ranking * Remove obsolete generated files * Rename tag index key to tag_key and reject reserved fields * Extract automatic tagging into a dedicated step * Restrict frontmatter updates to authorized keys * Refine tag filtering and automatic memory tagging * Require underscore-separated tags in auto-tag prompts - Forbid spaces in tags and require underscores (e.g. sam_altman) in both English and Chinese auto_tag prompts, with English examples switched to English entities (OpenAI, gold) - Drop prompt-string assertions superseded by the new tagging rule - Merge construction/runtime tag_key validation tests into one parametrized case * Make max_tags_per_file configurable in auto-tag step * Consolidate tag index tests * Fix search test fixture lint warnings * Align tag contracts and index health behavior * Fall back when tag index is unavailable --------- Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
94 lines
4.4 KiB
Python
94 lines
4.4 KiB
Python
"""``frontmatter_update_step`` — set frontmatter keys on a markdown file.
|
|
|
|
Read-modify-write the YAML frontmatter; body content is untouched.
|
|
The watcher / parser pick up the change asynchronously.
|
|
|
|
Input shape: ``frontmatter_update_step path=foo.md metadata={"x": "y", "z": "w"}``
|
|
— ``metadata`` is an explicit dict whose entries are merged into the
|
|
file's frontmatter (existing keys overwritten, missing keys inserted).
|
|
|
|
``path`` is a path relative to the workspace. Non-markdown targets return
|
|
``error="not markdown"``. An empty or missing ``metadata`` returns
|
|
``error="no fields to update"``.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import frontmatter
|
|
|
|
from ._file_io import get_path_lock
|
|
from ._path import _check_path_permission, display_path, gate_md, resolve_path
|
|
from ..base_step import BaseStep
|
|
from ...components import R
|
|
|
|
|
|
@R.register("frontmatter_update_step")
|
|
class FrontmatterUpdateStep(BaseStep):
|
|
"""Set frontmatter keys on a markdown file from a ``metadata`` dict.
|
|
|
|
Permission: honors the request-scoped ``_allowed_paths`` constraint
|
|
injected by the server into the RuntimeContext;
|
|
without it, restricting read/edit/write alone would still leave
|
|
frontmatter of arbitrary workspace Markdown files mutable.
|
|
|
|
``_allowed_frontmatter_keys`` optionally limits updates to an injected
|
|
list of top-level keys. Omitting it (or setting it to ``None``) preserves
|
|
the unrestricted historical behavior.
|
|
"""
|
|
|
|
async def execute(self):
|
|
assert self.context is not None
|
|
path: str = self.context.get("path") or ""
|
|
assert path, "path is required"
|
|
metadata = self.context.get("metadata") or {}
|
|
assert isinstance(metadata, dict), "metadata must be a dict"
|
|
|
|
allowed_keys = self.context.get("_allowed_frontmatter_keys")
|
|
key_error: str | None = None
|
|
if allowed_keys is not None:
|
|
if not isinstance(allowed_keys, list) or any(not isinstance(key, str) for key in allowed_keys):
|
|
key_error = "_allowed_frontmatter_keys must be a list of strings"
|
|
else:
|
|
allowed = set(allowed_keys)
|
|
denied = [key for key in metadata if not isinstance(key, str) or key not in allowed]
|
|
if denied:
|
|
names = ", ".join(sorted(repr(key) for key in denied))
|
|
key_error = f"frontmatter key(s) not allowed: {names}"
|
|
|
|
workspace_dir = Path(self.file_store.workspace_path or ".").resolve()
|
|
target, err = resolve_path(workspace_dir, path)
|
|
if err or target is None:
|
|
payload: dict = {"path": path, "error": err or "invalid path"}
|
|
else:
|
|
original_target = target
|
|
target, is_md = gate_md(target)
|
|
if not _check_path_permission(workspace_dir, target, self.context.get("_allowed_paths")):
|
|
payload = {"path": path, "error": "no permission to update this file"}
|
|
else:
|
|
lock = await get_path_lock(target)
|
|
async with lock:
|
|
if not target.is_file():
|
|
payload = {"path": path, "error": f"{display_path(workspace_dir, target)} not found"}
|
|
elif not is_md:
|
|
payload = {"path": path, "error": "not markdown"}
|
|
elif not metadata:
|
|
payload = {"path": path, "error": "no fields to update"}
|
|
elif key_error:
|
|
payload = {"path": path, "error": key_error}
|
|
else:
|
|
post = frontmatter.loads(target.read_text(encoding="utf-8"))
|
|
post.metadata.update(metadata)
|
|
target.write_text(frontmatter.dumps(post), encoding="utf-8")
|
|
payload = {"path": path, "updated": metadata}
|
|
if target != original_target:
|
|
payload["resolved_path"] = display_path(workspace_dir, target)
|
|
|
|
if "error" in payload:
|
|
self.context.response.success = False
|
|
self.context.response.answer = f"Error: {payload['error']}"
|
|
self.logger.info(f"[{self.name}] update failed path={path} error={payload['error']!r}")
|
|
else:
|
|
self.context.response.success = True
|
|
self.context.response.answer = f"Updated {len(metadata)} key(s) on {path}"
|
|
self.logger.info(f"[{self.name}] path={path} keys={list(metadata.keys())}")
|
|
self.context.response.metadata.update(payload)
|