mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 00:16:57 +00:00
search_for_pattern: add a snippet stage to the overflow shortening chain (#1667)
This adds an intermediate stage returning line numbers plus the matched text truncated to 60 characters. Test contributed by @AmirF194 in review.
This commit is contained in:
1 parent
369e3a05a0
commit
7c34662996
3 files changed
+98
-6
No files matched your search
@@ -21,6 +21,9 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
resolved to the start of the following line. The line the match ends on is now determined correctly,
|
||||
which also keeps `context_lines_after` aligned.
|
||||
- `safe_delete`: Add heuristic to delete superfluous empty lines after a deletion
|
||||
- `search_for_pattern`: on overflow, the shortening chain now emits each match's first line (full when
|
||||
it fits, otherwise truncated with a trailing '...' and a note) before falling back to bare line
|
||||
numbers, so agents can pick the right match without re-reading files. #1640
|
||||
|
||||
* JetBrains:
|
||||
- Allow external files from dependencies (specified via references like "<ext:FileUtil.class|472e0a13>") to be
|
||||
|
||||
@@ -611,15 +611,48 @@ class SearchForPatternTool(Tool):
|
||||
file_to_matches[match.source_file_path].append(match.to_display_string())
|
||||
|
||||
# capture lightweight match data for shortening before serialization
|
||||
match_lines_by_file: dict[str, list[int]] = defaultdict(list)
|
||||
match_lines_by_file: dict[str, list[dict[str, int | str]]] = defaultdict(list)
|
||||
for match in matches:
|
||||
assert match.source_file_path is not None
|
||||
match_lines_by_file[match.source_file_path].append(match.matched_lines[0].line_number)
|
||||
first = match.matched_lines[0]
|
||||
match_lines_by_file[match.source_file_path].append({"line": first.line_number, "text": first.line_content.strip()})
|
||||
|
||||
# shortened result closures, from least to most aggressive shortening
|
||||
def make_lines_only() -> str:
|
||||
"""Match locations without surrounding context"""
|
||||
return f"Match lines per file:\n{self._to_json(match_lines_by_file)}"
|
||||
_TEXT_TRUNCATE = 60
|
||||
|
||||
def render_first_lines(truncate: bool) -> str:
|
||||
"""Render each match's first line, either in full or truncated to a fixed length."""
|
||||
|
||||
def entry_text(text: str) -> str:
|
||||
if truncate and len(text) > _TEXT_TRUNCATE:
|
||||
return text[:_TEXT_TRUNCATE] + "..."
|
||||
return text
|
||||
|
||||
compact = {
|
||||
path: [{"line": m["line"], "text": entry_text(str(m["text"]))} for m in lines]
|
||||
for path, lines in match_lines_by_file.items()
|
||||
}
|
||||
if truncate:
|
||||
header = (
|
||||
f"Matched lines (text over {_TEXT_TRUNCATE} chars is truncated, marked with a trailing '...'); "
|
||||
"use read_file with the line numbers for full content:"
|
||||
)
|
||||
else:
|
||||
header = "Matched lines per file; use read_file with the line numbers for surrounding context:"
|
||||
return f"{header}\n{self._to_json(compact)}"
|
||||
|
||||
def make_first_lines_full() -> str:
|
||||
"""Match locations with each match's full first line."""
|
||||
return render_first_lines(truncate=False)
|
||||
|
||||
def make_first_lines_truncated() -> str:
|
||||
"""Match locations with each match's first line truncated to a fixed length."""
|
||||
return render_first_lines(truncate=True)
|
||||
|
||||
def make_line_numbers_only() -> str:
|
||||
"""Match locations as bare line numbers (no text)."""
|
||||
numbers = {path: [m["line"] for m in lines] for path, lines in match_lines_by_file.items()}
|
||||
return f"Match lines per file:\n{self._to_json(numbers)}"
|
||||
|
||||
def make_per_file_counts() -> str:
|
||||
counts = {path: len(lines) for path, lines in match_lines_by_file.items()}
|
||||
@@ -630,5 +663,13 @@ class SearchForPatternTool(Tool):
|
||||
|
||||
result = self._to_json(file_to_matches)
|
||||
return self._limit_length(
|
||||
result, max_answer_chars, shortened_result_factories=[make_lines_only, make_per_file_counts, make_summary]
|
||||
result,
|
||||
max_answer_chars,
|
||||
shortened_result_factories=[
|
||||
make_first_lines_full,
|
||||
make_first_lines_truncated,
|
||||
make_line_numbers_only,
|
||||
make_per_file_counts,
|
||||
make_summary,
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Tests for the ``SearchForPatternTool`` overflow shortening chain.
|
||||
|
||||
The snippet stage and, in particular, its position in the shortening chain were
|
||||
previously untested. Test contributed by @AmirF194 in review of PR #1667.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.project import Project
|
||||
from serena.tools.file_tools import SearchForPatternTool
|
||||
|
||||
|
||||
def test_search_for_pattern_snippet_stage(tmp_path):
|
||||
lines: list[str] = []
|
||||
for i in range(60):
|
||||
lines += [
|
||||
"filler above",
|
||||
"filler above",
|
||||
f"MATCHME item number {i:04d} " + "payload " * 6,
|
||||
"filler below",
|
||||
"filler below",
|
||||
]
|
||||
(tmp_path / "data.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
project = Project.load(str(tmp_path), serena_config=SerenaConfig(gui_log_window=False, web_dashboard=False))
|
||||
agent = MagicMock()
|
||||
agent.get_active_project_or_raise.return_value = project
|
||||
tool = SearchForPatternTool(agent)
|
||||
|
||||
def run(cap: int) -> str:
|
||||
return tool.apply(
|
||||
substring_pattern="MATCHME",
|
||||
context_lines_before=2,
|
||||
context_lines_after=2,
|
||||
restrict_search_to_code_files=False,
|
||||
max_answer_chars=cap,
|
||||
)
|
||||
|
||||
# wide but overflowing cap: the snippet stage (line + matched text) is returned
|
||||
snippet = run(7000)
|
||||
assert "The answer is too long" in snippet
|
||||
assert '"text":' in snippet and "MATCHME item number 0000" in snippet
|
||||
assert "Match lines per file" not in snippet # not the bare-line-numbers stage
|
||||
|
||||
# tighter cap: the chain degrades past the snippet stage to bare line numbers
|
||||
bare = run(1000)
|
||||
assert "Match lines per file" in bare and '"text":' not in bare
|
||||
Reference in new issue
Block a user