mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 05:50:44 +08:00
feat(memory): improve wiki search observability and retrieval flow
Expand memory wiki search with context-rich paginated results and bounded query expansion, and add default three-stage retrieval guidance for agents using memory_wiki_search. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d731288ffa
commit
5fc0805c69
4 changed files with 246 additions and 10 deletions
|
|
@ -128,6 +128,78 @@ def _read_lines(path: Path) -> list[str]:
|
|||
return text.splitlines()
|
||||
|
||||
|
||||
def _clip_text(s: str, *, max_chars: int = 240) -> str:
|
||||
t = str(s or "").strip()
|
||||
if len(t) <= max_chars:
|
||||
return t
|
||||
return t[: max(1, max_chars - 15)] + "...<truncated>"
|
||||
|
||||
|
||||
def _tokens_for_query(raw_query: str) -> list[str]:
|
||||
q = str(raw_query or "").strip()
|
||||
if not q:
|
||||
return []
|
||||
# Keep CJK runs, alpha words, and numbers.
|
||||
parts = re.findall(r"[\u4e00-\u9fff]+|[A-Za-z]+|\d+", q)
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for p in parts:
|
||||
tok = str(p or "").strip()
|
||||
key = tok.lower()
|
||||
if not tok or key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(tok)
|
||||
return out
|
||||
|
||||
|
||||
def _expand_query_variants(raw_query: str) -> list[str]:
|
||||
query = str(raw_query or "").strip()
|
||||
if not query:
|
||||
return []
|
||||
variants: list[str] = [query]
|
||||
synonym_map: dict[str, list[str]] = {
|
||||
"用户": ["创作者", "项目所有者", "owner", "profile", "identity"],
|
||||
"身份": ["角色", "identity", "profile"],
|
||||
"创建者": ["创作者", "所有者", "owner"],
|
||||
"owner": ["所有者", "项目所有者", "创作者"],
|
||||
"creator": ["创作者", "创建者", "项目所有者"],
|
||||
"name": ["名字", "姓名"],
|
||||
}
|
||||
toks = _tokens_for_query(query)
|
||||
for tok in toks:
|
||||
for syn in synonym_map.get(tok.lower(), []):
|
||||
if syn not in variants:
|
||||
variants.append(syn)
|
||||
if len(toks) > 1:
|
||||
joined = " ".join(toks)
|
||||
if joined not in variants:
|
||||
variants.append(joined)
|
||||
return variants
|
||||
|
||||
|
||||
def _build_line_hit(
|
||||
*,
|
||||
lines: list[str],
|
||||
line_idx: int,
|
||||
rel_path: str,
|
||||
matched_query: str,
|
||||
context_lines: int,
|
||||
) -> dict[str, Any]:
|
||||
before_start = max(0, line_idx - context_lines)
|
||||
before_lines = lines[before_start:line_idx]
|
||||
after_end = min(len(lines), line_idx + 1 + context_lines)
|
||||
after_lines = lines[line_idx + 1 : after_end]
|
||||
return {
|
||||
"path": rel_path,
|
||||
"line": int(line_idx + 1),
|
||||
"text": _clip_text(lines[line_idx]),
|
||||
"before": [_clip_text(x) for x in before_lines],
|
||||
"after": [_clip_text(x) for x in after_lines],
|
||||
"matched_query": str(matched_query or ""),
|
||||
}
|
||||
|
||||
|
||||
def _wiki_status(rt: WikiRuntime, args: dict[str, Any]) -> dict[str, Any]:
|
||||
del args
|
||||
files = _list_md_files(rt.wiki_root)
|
||||
|
|
@ -171,17 +243,69 @@ def _wiki_search(rt: WikiRuntime, args: dict[str, Any]) -> dict[str, Any]:
|
|||
is_regex = bool(args.get("is_regex"))
|
||||
req_limit = int(args.get("limit") or rt.max_search_results)
|
||||
limit = max(1, min(req_limit, rt.max_search_results))
|
||||
offset = max(0, int(args.get("offset") or 0))
|
||||
context_lines = max(0, min(int(args.get("context_lines") or 0), 5))
|
||||
path_prefix = str(args.get("path_prefix") or "").strip().replace("\\", "/").lstrip("/")
|
||||
expand_query = bool(args.get("expand_query")) and not is_regex
|
||||
max_rounds = max(1, min(int(args.get("max_rounds") or 2), 5))
|
||||
|
||||
queries = [query]
|
||||
if expand_query:
|
||||
queries = _expand_query_variants(query)[:max_rounds]
|
||||
|
||||
flags = 0 if case_sensitive else re.IGNORECASE
|
||||
pattern = re.compile(query if is_regex else re.escape(query), flags=flags)
|
||||
hits: list[dict[str, Any]] = []
|
||||
for fp in _list_md_files(rt.wiki_root):
|
||||
rel = str(fp.relative_to(rt.wiki_root)).replace("\\", "/")
|
||||
for idx, line in enumerate(_read_lines(fp), start=1):
|
||||
if pattern.search(line):
|
||||
hits.append({"path": rel, "line": idx, "text": line.strip()})
|
||||
if len(hits) >= limit:
|
||||
return {"ok": True, "query": query, "hits": hits, "truncated": True}
|
||||
return {"ok": True, "query": query, "hits": hits, "truncated": False}
|
||||
hits_all: list[dict[str, Any]] = []
|
||||
seen_keys: set[tuple[str, int]] = set()
|
||||
files_scanned = 0
|
||||
|
||||
for qv in queries:
|
||||
pattern = re.compile(qv if is_regex else re.escape(qv), flags=flags)
|
||||
for fp in _list_md_files(rt.wiki_root):
|
||||
rel = str(fp.relative_to(rt.wiki_root)).replace("\\", "/")
|
||||
if path_prefix and not rel.startswith(path_prefix):
|
||||
continue
|
||||
files_scanned += 1
|
||||
lines = _read_lines(fp)
|
||||
for idx, line in enumerate(lines):
|
||||
if not pattern.search(line):
|
||||
continue
|
||||
key = (rel, int(idx + 1))
|
||||
if key in seen_keys:
|
||||
continue
|
||||
seen_keys.add(key)
|
||||
hits_all.append(
|
||||
_build_line_hit(
|
||||
lines=lines,
|
||||
line_idx=idx,
|
||||
rel_path=rel,
|
||||
matched_query=qv,
|
||||
context_lines=context_lines,
|
||||
)
|
||||
)
|
||||
|
||||
hits_page = hits_all[offset : offset + limit]
|
||||
next_offset = offset + len(hits_page)
|
||||
truncated = next_offset < len(hits_all)
|
||||
query_used = str(hits_page[0].get("matched_query") or query) if hits_page else query
|
||||
|
||||
# Backward-compatible fields remain: ok/query/hits/truncated.
|
||||
# New fields are additive diagnostics/controls for iterative search flows.
|
||||
return {
|
||||
"ok": True,
|
||||
"query": query,
|
||||
"hits": hits_page,
|
||||
"truncated": bool(truncated),
|
||||
"query_used": query_used,
|
||||
"queries_attempted": queries,
|
||||
"total_hits_estimate": len(hits_all),
|
||||
"offset": offset,
|
||||
"next_offset": next_offset if truncated else None,
|
||||
"limit": limit,
|
||||
"context_lines": context_lines,
|
||||
"path_prefix": path_prefix,
|
||||
"files_scanned": files_scanned,
|
||||
"expanded": bool(expand_query),
|
||||
}
|
||||
|
||||
|
||||
def _wiki_lint(rt: WikiRuntime, args: dict[str, Any]) -> dict[str, Any]:
|
||||
|
|
@ -290,8 +414,13 @@ def build_wiki_tool_specs(api: Any) -> list[dict[str, Any]]:
|
|||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer"},
|
||||
"offset": {"type": "integer"},
|
||||
"is_regex": {"type": "boolean"},
|
||||
"case_sensitive": {"type": "boolean"},
|
||||
"context_lines": {"type": "integer"},
|
||||
"path_prefix": {"type": "string"},
|
||||
"expand_query": {"type": "boolean"},
|
||||
"max_rounds": {"type": "integer"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue