fix(step): more fixes for pr comments

This commit is contained in:
imrewce 2026-05-18 19:05:57 +08:00
parent 5677606875
commit c96a192d23
7 changed files with 19 additions and 61 deletions

View file

@ -42,7 +42,7 @@ reme4 version
| 🔎 search | 🔍 `search` (`search_step`) | `call_server("search", query=…, …)` | 📥 `query:str` 🎚️ `limit:int=5`(>0) 🎚️ `min_score:float=0.0` ⚖️ `vector_weight:float=0.7` ∈[0,1]keyword 权 = 1-vw 🔀 `candidate_multiplier:float=3.0`candidates = min(200, limit×mult) 🔗 `expand_links:bool=True` 🔢 `max_links_per_direction:int=10` 🎚️ `search_filter:dict={}` 📤 `answer` 每命中一行 `path:start-end [score=… vector=… keyword=…] text` + 缩进的 `→ outlinks (n)` / `← inlinks (n)` + `via predicate=… anchor=#…` 📊 `metadata.results` / `metadata.link_expansion` / `metadata.counts={vector,keyword,returned,hybrid}` 🛠️ 并行 `vector_search` + `keyword_search` → RRF 融合K=60按 chunk.id 合并)→ `min_score` 过滤 → `limit` 截断 → 邻居 meta 注入 |
| 🧪 demo | 🪄 `demo_echo` (`demo_echo_step1` + `step2`) | `call_server("demo_echo", query=…, min_score=…)` | 📥 `query:str=""` 🎚️ `min_score:float=0.5` 🛠️ step1`processed_query = query.strip().lower()``adjusted_min_score = min_score * 0.9`,写回 context 📤 step2`answer = "echo: {processed_query} (min_score={adjusted_min_score})"` 📊 `metadata = {step, query, min_score, processed_query, adjusted_min_score}` |
| 🌊 demo | 🌊 `stream_demo` (`stream_demo_step1` + `step2`) | `call_server("stream_demo", query=…, repeat=…, interval=…)` | 📥 `query:str=""` 🎚️ `repeat:int=10` 🎚️ `interval:float=0.1`(秒/字符)| 🛠️ step1`stream_text = query * repeat` 写回 context 📤 step2按字符 `add_stream_string(ch, ChunkEnum.CONTENT)` 流式输出,`asyncio.sleep(interval)` 节流 |
| 📂 crud_md | 📖 `read` (`read_step`) | `call_server("read", path=…, …)` | 📥 `path:str` ⭐(**相对** `working_dir`,绝对路径会被拒绝;无后缀自动补 `.md``.md` 后缀拒绝)| 🎚️ `start_line:int=null`1-based, 含端点)| 🎚️ `end_line:int=null`1-based, 含端点)| 🎚️ `max_bytes:int=51200`(截断阈值)| 📤 `answer = 选中的行内容`,超过 `max_bytes` 时附加 `--- TRUNCATED ---` 续读指引(`start_line=…` 📊 `metadata.path` / `metadata.total_lines`(出错路径才会附带)| 🛠️ 流程:`BaseStep.resolve_path(raw)` → `aiofiles.os.stat``read_file_safe`utf-8-sig BOM 容忍、UnicodeDecodeError fallback `errors=ignore`)→ `split("\n")` 切片 `[s-1:e]``truncate_text_output` 按字节截断保行 |
| 📂 crud | 📖 `read` (`read_step`) | `call_server("read", path=…, …)` | 📥 `path:str` ⭐(**完整相对路径**,相对于 `working_dir`;绝对路径会被拒绝;`.md` 后缀拒绝)| 🎚️ `start_line:int=null`1-based, 含端点)| 🎚️ `end_line:int=null`1-based, 含端点)| 🎚️ `max_bytes:int=51200`(截断阈值)| 📤 `answer = 选中的行内容`,超过 `max_bytes` 时附加 `--- TRUNCATED ---` 续读指引(`start_line=…` 📊 `metadata.path` / `metadata.total_lines`(出错路径才会附带)| 🛠️ 流程:`BaseStep.resolve_path(raw, require_md=True)` → `aiofiles.os.stat``read_file_safe`utf-8-sig BOM 容忍、UnicodeDecodeError fallback `errors=ignore`)→ `split("\n")` 切片 `[s-1:e]``truncate_text_output` 按字节截断保行 |
使用示例:
@ -67,7 +67,7 @@ reme4 version
reme4 reindex
reme4 search query="latency 问题" limit=10 min_score=0.2 vector_weight=0.6
# 读取 working_dir 下的 markdown相对路径无后缀自动补 .md可按行切片或限制字节
# 读取 working_dir 下的 markdown完整相对路径;无后缀自动补 .md可按行切片或限制字节
reme4 read path=Templates/Recipe.md
reme4 read path=Notes start_line=1 end_line=20
reme4 read path=Big.md max_bytes=4096

View file

@ -98,21 +98,19 @@ jobs:
- backend: base
name: read
description: "read a markdown file (relative to working_dir, no .md suffix needed)"
description: "read a markdown file (relative path under working_dir)"
parameters:
type: object
properties:
path:
type: string
description: "relative path rooted at the working_dir; markdown only (.md auto-appended when no suffix)"
description: "relative path under the working_dir (no absolute paths); markdown only"
start_line:
type: integer
description: "first line to read (1-based, inclusive)"
default: null
description: "Optional, first line to read (1-based, inclusive)"
end_line:
type: integer
description: "last line to read (1-based, inclusive)"
default: null
description: "Optional, last line to read (1-based, inclusive)"
required:
- path
steps:

View file

@ -1,11 +1,11 @@
"""steps"""
from . import common
from . import crud_md
from . import crud
from .base_step import BaseStep
__all__ = [
"common",
"crud_md",
"crud",
"BaseStep",
]

View file

@ -91,15 +91,15 @@ class BaseStep(ABC):
self,
raw: str,
*,
require_md: bool = True,
require_md: bool = False,
) -> tuple[Path | None, str | None]:
"""Resolve a relative `path=` argument under self.working_path.
"""Resolve relative `path=` argument under self.working_path.
Rules:
- relative path only; joined under ``self.working_path``.
Markdown gate (when ``require_md=True``):
- no suffix auto-append ``.md``;
- any other non-``.md`` suffix reject.
- the caller supplies the full relative path under ``self.working_path``;
absolute paths are rejected.
- if the path has no suffix, auto-append ``.md`` (default vault extension).
- if ``require_md=True``, any present non-``.md`` suffix is rejected.
Returns ``(abs_path, None)`` on success, or ``(None, error_message)`` on failure.
"""
if not raw or not str(raw).strip():
@ -107,13 +107,12 @@ class BaseStep(ABC):
s = str(raw).strip()
p = Path(s)
if p.is_absolute():
return None, (f"path {s!r} is absolute; only relative paths under the working_dir are accepted")
return None, (f"path {s!r} is absolute; only relative paths accepted")
target = (self.working_path.resolve() / p).resolve()
if require_md:
if target.suffix == "":
target = target.with_suffix(".md")
elif target.suffix.lower() != ".md":
return None, (f"path {s!r} is not a markdown file; this command only supports .md files")
if target.suffix == "":
target = target.with_suffix(".md")
elif require_md and target.suffix.lower() != ".md":
return None, (f"path {s!r} is not a markdown file; this command only supports .md files")
return target, None
def _resolve(

View file

@ -1,7 +1,5 @@
"""Shared filesystem helpers for CRUD steps (safe read, truncation)."""
import re
import aiofiles
import aiofiles.os
@ -47,9 +45,6 @@ def truncate_text_output(
return text
try:
if TRUNCATION_NOTICE_MARKER in text:
return _retruncate(text, max_bytes=max_bytes, encoding=encoding)
text_bytes = text.encode(encoding)
if len(text_bytes) <= max_bytes:
return text
@ -77,37 +72,3 @@ def truncate_text_output(
except Exception:
logger.warning("truncate_text_output failed, returning original text", exc_info=True)
return text
def _retruncate(text: str, *, max_bytes: int, encoding: str) -> str:
parts = text.split(TRUNCATION_NOTICE_MARKER, 1)
original_content = parts[0]
old_notice = parts[1]
text_bytes = original_content.encode(encoding)
if len(text_bytes) <= max_bytes + 100:
return text
start_match = re.search(r"starts at line (\d+)", old_notice)
if not start_match:
return text
start_line_parsed = int(start_match.group(1))
truncated_bytes = text_bytes[:max_bytes]
result = truncated_bytes.decode(encoding, errors="ignore")
newline_count = result.count("\n")
next_line = start_line_parsed + max(1, newline_count)
if not re.search(r"covers the next \d+ bytes", old_notice):
return text
new_notice = re.sub(
r"covers the next \d+ bytes",
f"covers the next {max_bytes} bytes",
old_notice,
)
new_notice = re.sub(
r"start_line=\d+ to read more",
f"start_line={next_line} to read more",
new_notice,
)
return result + TRUNCATION_NOTICE_MARKER + new_notice