Skip to content

feat(search): grep fallback when semantic recall is empty - #2938

Open
lg320531124 wants to merge 1 commit into
volcengine:mainfrom
lg320531124:feat/search-grep-fallback
Open

feat(search): grep fallback when semantic recall is empty#2938
lg320531124 wants to merge 1 commit into
volcengine:mainfrom
lg320531124:feat/search-grep-fallback

Conversation

@lg320531124

Copy link
Copy Markdown
Contributor

Problem

MemorySearchTool.execute returns [] the moment semantic search yields no memories. The agent has no escape hatch: the memory toolset exposes only read / search / ls (no grep), so a query the embedder or index fumbled is a dead end even when the literal string exists verbatim in stored content.

search_result = await ctx.viking_fs.search(query, ...)
return optimize_search_result(search_result.to_dict(), limit=limit)   # [] if empty

Change

When semantic recall is empty, retry with a literal grep:

if query:
    grep_result = await ctx.viking_fs.grep(
        target_uri,
        pattern=re.escape(query),     # literal substring, not regex
        case_insensitive=True,        # belt-and-suspenders over query casefold
        node_limit=limit + 10,        # match search over-sampling
        ctx=request_ctx,
    )
grep_memories = [{"uri": m["uri"], "score": 0.0, "line": m["line"], "content": m["content"]}
                 for m in grep_result["matches"] if m.get("uri")]
if grep_memories:
    result_dict = {"memories": grep_memories}
  • re.escape(query) → matches as a literal substring, not interpreted as regex (a query like a.b*c+ would otherwise be a regex that matches almost nothing, or worse, everything)
  • case_insensitive=True is redundant over the query-side casefold from feat(search): normalize semantic query (NFKC + casefold) #2937 but harmless — they act on different sides (query vs indexed content)
  • score=0.0 honestly marks "no relevance score, literal match" rather than fabricating a ranking score
  • optimize_search_result then filters .abstract.md / .overview.md and truncates to limit uniformly — no new code path in the optimizer

Guarded edges

  • Empty query skips grepre.escape("") == "" matches everything; guard prevents that
  • grep exceptions swallowed + logged — a VikingDB outage during fallback degrades to [], not a propagated crash

Relationship to #2900

Orthogonal, not overlapping:

Both can land independently; together they make the full path robust.

Tests

tests/unit/test_search_grep_fallback.py — 6 cases, all pass:

Case Asserts
search has results grep NOT called (no false fallback)
search empty → grep hits matches map to {uri, score}; pattern is escaped; case_insensitive=True
both empty returns []
grep raises returns [], no propagation
empty query grep NOT called
regex metachars a.b*c+a\.b\*c\+
======================== 6 passed, 4 warnings in 2.07s =========================

Pure unit tests (mocked ctx.viking_fs), no vectordb/embedder fixture — the tool logic is side-effect-free string/URI handling.

Scope

+37 lines in tools.py (one method body), 1 new test file. No new tool class, no public API change, no config knob.

Related

Search-recall robustness line: #2937 (query normalization) → this PR (semantic→grep fallback) → #2900 (grep self-hardening, storage layer).

@lg320531124

Copy link
Copy Markdown
Contributor Author

CI note: the API & CLI Integration Tests job is red, but this is a pre-existing CI-infra flake, not introduced by this PR.

The failure is identical across all currently-open PRs (#2874, #2936, #2937, #2938) and reproduces on #2874 which predates my other work:

  1. Build ragfs-python native extension❌ ERROR: maturin build produced no wheel (the Rust→Python binding fails to build a wheel in CI)
  2. test_cli_search.py::TestSearchGrep::test_grep_basic / test_grep_case_insensitive → ERROR at the add-resource setup step (AssertionError: add-resource failed after retries)
  3. find/search tests → SKIPPED (Upstream ... marker — the upstream VikingDB service is unreachable from the runner)

This PR is pure-Python (touches only 'openviking/session/memory/tools.py'-style modules), does not touch crates/, maturin, the ov CLI binary, or any CI workflow — so it cannot be the cause of a Rust-binding build failure or an upstream-service reachability issue.

Evidence that maintainers are not blocking on this flake: #2934 was merged today (2026-07-02 01:07 UTC) with the same API & CLI Integration Tests → FAILURE status, and check-deps → SUCCESS + main-branch 02. Main Branch Checks → success both stay green.

Local unit tests for this PR pass (see PR body). Happy to help root-cause the maturin/upstream flake separately if useful — just let me know.

MemorySearchTool.execute returned [] as soon as semantic search yielded
no memories, leaving the agent with no escape hatch — there is no grep
tool exposed in the memory toolset (only read/search/ls), so a query
that the embedder or index failed on was a dead end even when the
literal string exists in stored content.

Retry with a literal grep when semantic recall is empty:
  - pattern = re.escape(query)  # match as literal substring, not regex
  - case_insensitive=True       # belt-and-suspenders over query casefold
  - node_limit = limit + 10     # match the over-sampling search already does
grep matches ({line, uri, content}) are projected onto the {uri, score}
memory shape (score=0.0 honestly marks "no relevance score, literal
match") so optimize_search_result can filter .abstract.md/.overview.md
and truncate uniformly.

Guarded edges:
  - empty query skips grep (re.escape('') == '' would match everything)
  - grep exceptions are swallowed and logged; the tool still returns []
    instead of propagating a partial failure

This is orthogonal to volcengine#2900, which hardens viking_fs.grep itself
(VikingDB timeout/empty -> fs). This PR is the Tool-layer complement:
when semantic search *as a whole* returns nothing, drop to grep.

Tests: tests/unit/test_search_grep_fallback.py (6 cases, all pass) cover
results-present (no grep), empty->grep mapping, both-empty, grep raises,
empty query, and regex-escaping of metacharacters.
@lg320531124
lg320531124 force-pushed the feat/search-grep-fallback branch from 94c1d32 to c649a42 Compare July 2, 2026 23:28
@huangruiteng

huangruiteng commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

@qin-ctx 更正我上一版关系说明:#2874 本身是独立的 tool-input-compaction PR,不是本 PR 的 issue;#2938 是独立的 semantic-search→grep fallback PR。

我对 exact head c649a42 重新做了 current-main(4295dfd) 合成验证:无冲突,focused tests 6/6 通过,Ruff format 通过。但目前有一个运行时 blocker:ToolContext.default_search_uris 的真实类型是 List[str],而 VikingFS.grep(uri=...) 只接受 str;新增测试把 default_search_uris 模拟成了字符串,掩盖了这个类型错配。请按每个 scope URI 逐一 grep(或明确选择一个 URI并说明语义),并用真实 ToolContext 覆盖多 URI/空 URI;另外删除未使用的 pytest 导入,当前 Ruff check 报 F401。

这些修正完成后再请求 review;当前不建议合并。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants