* feat: enhance file mention parsing with deduplication and warning handling * feat: optimize ancestor grouping by improving path comparison efficiency * fix: update middleware injection to use extend for better readability * Refactor code structure for improved readability and maintainability * feat: update README files to include AstaBench ranking and adjust award image layout * update * fix: deduplicate file mentions and improve warning message formatting
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 370 KiB |
@@ -141,8 +141,8 @@ def _inject_subagent_middleware(subs: list[dict]) -> None:
|
|||||||
from .middleware import ContextOverflowMapperMiddleware, ToolErrorHandlerMiddleware
|
from .middleware import ContextOverflowMapperMiddleware, ToolErrorHandlerMiddleware
|
||||||
|
|
||||||
for sa in subs:
|
for sa in subs:
|
||||||
sa.setdefault("middleware", []).append(
|
sa.setdefault("middleware", []).extend(
|
||||||
ToolErrorHandlerMiddleware(), ContextOverflowMapperMiddleware()
|
[ToolErrorHandlerMiddleware(), ContextOverflowMapperMiddleware()]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,10 @@ def _read_file(path: Path) -> str:
|
|||||||
return f"\n### {path.name}\nPath: `{path}`\n```\n{content}\n```"
|
return f"\n### {path.name}\nPath: `{path}`\n```\n{content}\n```"
|
||||||
|
|
||||||
|
|
||||||
def parse_file_mentions(text: str, cwd: Path | None = None) -> list[Path]:
|
def parse_file_mentions(
|
||||||
|
text: str,
|
||||||
|
cwd: Path | None = None,
|
||||||
|
) -> tuple[list[Path], list[str]]:
|
||||||
"""Extract resolved ``@file`` paths from *text*.
|
"""Extract resolved ``@file`` paths from *text*.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -191,13 +194,19 @@ def parse_file_mentions(text: str, cwd: Path | None = None) -> list[Path]:
|
|||||||
process working directory.
|
process working directory.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of resolved, existing ``Path`` objects (directories excluded).
|
``(files, warnings)`` — deduplicated list of resolved, existing
|
||||||
Unresolvable or missing paths are skipped with a printed warning.
|
``Path`` objects (directories excluded) in order of first appearance,
|
||||||
|
and a list of human-readable warning strings to be displayed by the
|
||||||
|
caller. Callers must display the warnings themselves using the
|
||||||
|
appropriate UI mechanism (Rich console, Textual widget, etc.).
|
||||||
"""
|
"""
|
||||||
if cwd is None:
|
if cwd is None:
|
||||||
cwd = Path.cwd()
|
cwd = Path.cwd()
|
||||||
|
|
||||||
|
workspace_root = cwd.resolve()
|
||||||
files: list[Path] = []
|
files: list[Path] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
seen: set[Path] = set()
|
||||||
for match in FILE_MENTION_PATTERN.finditer(text):
|
for match in FILE_MENTION_PATTERN.finditer(text):
|
||||||
# Skip email addresses — character immediately before @ is alphanumeric
|
# Skip email addresses — character immediately before @ is alphanumeric
|
||||||
before = text[: match.start()]
|
before = text[: match.start()]
|
||||||
@@ -212,21 +221,35 @@ def parse_file_mentions(text: str, cwd: Path | None = None) -> list[Path]:
|
|||||||
if not p.is_absolute():
|
if not p.is_absolute():
|
||||||
p = cwd / p
|
p = cwd / p
|
||||||
resolved = p.resolve()
|
resolved = p.resolve()
|
||||||
if resolved.exists() and resolved.is_file():
|
if not resolved.exists() or not resolved.is_file():
|
||||||
files.append(resolved)
|
warnings.append(f"@file not found: {raw}")
|
||||||
else:
|
continue
|
||||||
print(f"[warning] @file not found: {raw}")
|
# Deduplicate: skip paths already seen in this message.
|
||||||
|
if resolved in seen:
|
||||||
|
continue
|
||||||
|
seen.add(resolved)
|
||||||
|
files.append(resolved)
|
||||||
|
# Warn when the file lives outside the workspace root — it may
|
||||||
|
# contain sensitive content (e.g. @~/.ssh/id_rsa).
|
||||||
|
# Checked after dedup so a repeated mention only warns once.
|
||||||
|
try:
|
||||||
|
resolved.relative_to(workspace_root)
|
||||||
|
except ValueError:
|
||||||
|
warnings.append(
|
||||||
|
f"@{raw} is outside the workspace "
|
||||||
|
f"({workspace_root}) — embedding may expose sensitive files"
|
||||||
|
)
|
||||||
except (OSError, RuntimeError) as exc:
|
except (OSError, RuntimeError) as exc:
|
||||||
print(f"[warning] invalid @file path {raw!r}: {exc}")
|
warnings.append(f"invalid @file path {raw!r}: {exc}")
|
||||||
|
|
||||||
return files
|
return files, warnings
|
||||||
|
|
||||||
|
|
||||||
def resolve_file_mentions(
|
def resolve_file_mentions(
|
||||||
text: str,
|
text: str,
|
||||||
workspace_dir: str | None = None,
|
workspace_dir: str | None = None,
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str, list[str]]:
|
||||||
"""Parse ``@file`` mentions and return *(original_text, final_prompt)*.
|
"""Parse ``@file`` mentions and return *(original_text, final_prompt, warnings)*.
|
||||||
|
|
||||||
*final_prompt* equals *original_text* when no valid mentions are found,
|
*final_prompt* equals *original_text* when no valid mentions are found,
|
||||||
otherwise it appends a ``## Referenced Files`` section with the file
|
otherwise it appends a ``## Referenced Files`` section with the file
|
||||||
@@ -237,14 +260,15 @@ def resolve_file_mentions(
|
|||||||
workspace_dir: Workspace root used for resolving relative paths.
|
workspace_dir: Workspace root used for resolving relative paths.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
``(original_text, final_prompt)`` — the first element is always the
|
``(original_text, final_prompt, warnings)`` — the first element is
|
||||||
unchanged input; the second is the prompt to send to the agent.
|
always the unchanged input; the second is the prompt to send to the
|
||||||
|
agent; the third is a list of warning strings to display to the user.
|
||||||
"""
|
"""
|
||||||
cwd = Path(workspace_dir) if workspace_dir else None
|
cwd = Path(workspace_dir) if workspace_dir else None
|
||||||
files = parse_file_mentions(text, cwd=cwd)
|
files, warnings = parse_file_mentions(text, cwd=cwd)
|
||||||
|
|
||||||
if not files:
|
if not files:
|
||||||
return text, text
|
return text, text, warnings
|
||||||
|
|
||||||
parts = [text, "\n\n## Referenced Files\n"]
|
parts = [text, "\n\n## Referenced Files\n"]
|
||||||
for path in files:
|
for path in files:
|
||||||
@@ -253,7 +277,7 @@ def resolve_file_mentions(
|
|||||||
except (OSError, UnicodeDecodeError) as exc:
|
except (OSError, UnicodeDecodeError) as exc:
|
||||||
parts.append(f"\n### {path.name}\n[Error reading file: {exc}]")
|
parts.append(f"\n### {path.name}\n[Error reading file: {exc}]")
|
||||||
|
|
||||||
return text, "\n".join(parts)
|
return text, "\n".join(parts), warnings
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -922,11 +922,15 @@ def cmd_interactive(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Resolve @file mentions — inject file contents inline
|
# Resolve @file mentions — inject file contents inline
|
||||||
_, message_to_send = resolve_file_mentions(
|
_, message_to_send, file_warnings = resolve_file_mentions(
|
||||||
user_input, state["workspace_dir"]
|
user_input, state["workspace_dir"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stream agent response with metadata for persistence
|
# Stream agent response with metadata for persistence
|
||||||
|
# Warnings printed here so they appear just before the
|
||||||
|
# model response, not before the user input echo.
|
||||||
|
for w in file_warnings:
|
||||||
|
console.print(f"[yellow]⚠ {escape(w)}[/yellow]")
|
||||||
console.print()
|
console.print()
|
||||||
meta = build_metadata(state["workspace_dir"], model)
|
meta = build_metadata(state["workspace_dir"], model)
|
||||||
run_streaming(
|
run_streaming(
|
||||||
|
|||||||
@@ -700,6 +700,7 @@ def run_textual_interactive(
|
|||||||
on_todo_cb: Callable[[list[dict]], None] | None = None,
|
on_todo_cb: Callable[[list[dict]], None] | None = None,
|
||||||
on_media_cb: Callable[[str], None] | None = None,
|
on_media_cb: Callable[[str], None] | None = None,
|
||||||
skip_user_message: bool = False,
|
skip_user_message: bool = False,
|
||||||
|
file_warnings: list[str] | None = None,
|
||||||
channel_hitl_fn: Callable[[list], list[dict] | None] | None = None,
|
channel_hitl_fn: Callable[[list], list[dict] | None] | None = None,
|
||||||
channel_ask_user_fn: Callable[[dict], dict] | None = None,
|
channel_ask_user_fn: Callable[[dict], dict] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -723,6 +724,10 @@ def run_textual_interactive(
|
|||||||
# 1. Mount user message + loading spinner
|
# 1. Mount user message + loading spinner
|
||||||
if not skip_user_message:
|
if not skip_user_message:
|
||||||
await container.mount(UserMessage(user_text))
|
await container.mount(UserMessage(user_text))
|
||||||
|
# Mount file warnings after user message so they appear in the
|
||||||
|
# correct position (between user input and model response).
|
||||||
|
for w in file_warnings or []:
|
||||||
|
self._append_system(f"⚠ {w}", style="yellow")
|
||||||
loading = LoadingWidget()
|
loading = LoadingWidget()
|
||||||
await container.mount(loading)
|
await container.mount(loading)
|
||||||
container.scroll_end(animate=False)
|
container.scroll_end(animate=False)
|
||||||
@@ -1388,13 +1393,17 @@ def run_textual_interactive(
|
|||||||
self._render_status()
|
self._render_status()
|
||||||
cancelled = False
|
cancelled = False
|
||||||
|
|
||||||
# Resolve @file mentions — inject file contents before sending to agent
|
# Resolve @file mentions — inject file contents before sending to agent.
|
||||||
_, message_to_send = await asyncio.to_thread(
|
# Use self._workspace_dir (current session) not the startup-captured
|
||||||
resolve_file_mentions, user_text, workspace_dir
|
# workspace_dir closure, which becomes stale after /new or /resume.
|
||||||
|
_, message_to_send, file_warnings = await asyncio.to_thread(
|
||||||
|
resolve_file_mentions, user_text, self._workspace_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._stream_with_widgets(message_to_send)
|
await self._stream_with_widgets(
|
||||||
|
message_to_send, file_warnings=file_warnings
|
||||||
|
)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
cancelled = True
|
cancelled = True
|
||||||
self._append_system("\nInterrupted by user", style="dim italic #ffe082")
|
self._append_system("\nInterrupted by user", style="dim italic #ffe082")
|
||||||
@@ -2000,6 +2009,10 @@ def run_textual_interactive(
|
|||||||
lambda: setattr(self, "_quit_pending", False),
|
lambda: setattr(self, "_quit_pending", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def force_quit(self) -> None:
|
||||||
|
"""Exit immediately without double-press confirmation (used by /exit command)."""
|
||||||
|
self._do_exit()
|
||||||
|
|
||||||
def _do_exit(self) -> None:
|
def _do_exit(self) -> None:
|
||||||
"""Clean up channels and exit."""
|
"""Clean up channels and exit."""
|
||||||
if self._channel_timer is not None:
|
if self._channel_timer is not None:
|
||||||
|
|||||||
@@ -73,19 +73,28 @@ def _group_by_ancestor(norm_paths: list[str]) -> dict[str, list[str]]:
|
|||||||
full path.
|
full path.
|
||||||
|
|
||||||
The returned dict is ordered by first appearance in *norm_paths*.
|
The returned dict is ordered by first appearance in *norm_paths*.
|
||||||
|
|
||||||
|
Complexity: O(n log n) — paths are sorted lexicographically so the
|
||||||
|
maximum common prefix depth for each path is found by comparing only
|
||||||
|
its immediate neighbours in sorted order (not all pairs).
|
||||||
"""
|
"""
|
||||||
path_to_ancestor: dict[str, str] = {}
|
sorted_paths = sorted(norm_paths)
|
||||||
for i, p in enumerate(norm_paths):
|
n = len(sorted_paths)
|
||||||
|
|
||||||
|
# Single pass over sorted list: max common prefix is always with a neighbour.
|
||||||
|
sorted_best: dict[str, int] = {}
|
||||||
|
for i, p in enumerate(sorted_paths):
|
||||||
best = 1 # at minimum depth 1 (~)
|
best = 1 # at minimum depth 1 (~)
|
||||||
for j, other in enumerate(norm_paths):
|
if i > 0:
|
||||||
if i != j:
|
best = max(best, _common_prefix_depth(p, sorted_paths[i - 1]))
|
||||||
best = max(best, _common_prefix_depth(p, other))
|
if i < n - 1:
|
||||||
# Only group if they truly share a meaningful ancestor (>= 2 levels)
|
best = max(best, _common_prefix_depth(p, sorted_paths[i + 1]))
|
||||||
if best >= 2:
|
sorted_best[p] = best
|
||||||
ancestor = "/".join(p.split("/")[:best])
|
|
||||||
else:
|
path_to_ancestor: dict[str, str] = {
|
||||||
ancestor = p # standalone
|
p: ("/".join(p.split("/")[:best]) if best >= 2 else p)
|
||||||
path_to_ancestor[p] = ancestor
|
for p, best in sorted_best.items()
|
||||||
|
}
|
||||||
|
|
||||||
groups: dict[str, list[str]] = {}
|
groups: dict[str, list[str]] = {}
|
||||||
for p in norm_paths:
|
for p in norm_paths:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class CommandUI(Protocol):
|
|||||||
) -> list | None: ...
|
) -> list | None: ...
|
||||||
def clear_chat(self) -> None: ...
|
def clear_chat(self) -> None: ...
|
||||||
def request_quit(self) -> None: ...
|
def request_quit(self) -> None: ...
|
||||||
|
def force_quit(self) -> None: ...
|
||||||
def start_new_session(self) -> None: ...
|
def start_new_session(self) -> None: ...
|
||||||
async def handle_session_resume(
|
async def handle_session_resume(
|
||||||
self, thread_id: str, workspace_dir: str | None = None
|
self, thread_id: str, workspace_dir: str | None = None
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ class ChannelCommandUI(CommandUI):
|
|||||||
def request_quit(self) -> None:
|
def request_quit(self) -> None:
|
||||||
self.append_system("Quit command ignored in channel.")
|
self.append_system("Quit command ignored in channel.")
|
||||||
|
|
||||||
|
def force_quit(self) -> None:
|
||||||
|
self.request_quit()
|
||||||
|
|
||||||
def start_new_session(self) -> None:
|
def start_new_session(self) -> None:
|
||||||
if self.start_new_session_callback:
|
if self.start_new_session_callback:
|
||||||
self.start_new_session_callback()
|
self.start_new_session_callback()
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ class ExitCommand(Command):
|
|||||||
description = "Quit EvoScientist"
|
description = "Quit EvoScientist"
|
||||||
|
|
||||||
async def execute(self, ctx: CommandContext, args: list[str]) -> None:
|
async def execute(self, ctx: CommandContext, args: list[str]) -> None:
|
||||||
ctx.ui.request_quit()
|
ctx.ui.force_quit()
|
||||||
|
|
||||||
|
|
||||||
# Register session commands
|
# Register session commands
|
||||||
|
|||||||
@@ -46,21 +46,26 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="33%">
|
<td align="center" valign="top" width="25%">
|
||||||
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/ICAIS_awards.JPG" height="180" alt="ICAIS 2025 Awards"/>
|
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/ICAIS_awards.JPG" height="180" alt="ICAIS 2025 Awards"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>Best Paper & Appraisal Award</b></sub>
|
<sub><b>Best Paper & Appraisal Award</b></sub>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="top" width="33%">
|
<td align="center" valign="top" width="25%">
|
||||||
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/ICAIS_best_paper.png" height="180" alt="Best Paper"/>
|
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/ICAIS_best_paper.png" height="180" alt="Best Paper"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>AI-Generated Best Paper</b></sub>
|
<sub><b>AI-Generated Best Paper</b></sub>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="top" width="33%">
|
<td align="center" valign="top" width="25%">
|
||||||
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/deepresearch_bench_2.JPG" height="180" alt="DeepResearch Bench II #1"/>
|
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/deepresearch_bench_2.JPG" height="180" alt="DeepResearch Bench II #1"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>#1 on DeepResearch Bench II</b></sub>
|
<sub><b>#1 on DeepResearch Bench II</b></sub>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="top" width="25%">
|
||||||
|
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/asta_bench_code.png" height="180" alt="AstaBench Code & Execution #1"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>#1 on AstaBench Code & Execution</b></sub>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -97,6 +102,7 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
|
|||||||
> Looking for ready-to-use research skills? Check out [**EvoSkills**](https://github.com/EvoScientist/EvoSkills) — powered by [**EvoScientist**](https://github.com/EvoScientist/EvoScientist)'s engine and installable skills, the entire end-to-end research lifecycle is covered out of the box. [**EvoSkills**](https://github.com/EvoScientist/EvoSkills) are also compatible with other CLI coding agents.
|
> Looking for ready-to-use research skills? Check out [**EvoSkills**](https://github.com/EvoScientist/EvoSkills) — powered by [**EvoScientist**](https://github.com/EvoScientist/EvoScientist)'s engine and installable skills, the entire end-to-end research lifecycle is covered out of the box. [**EvoSkills**](https://github.com/EvoScientist/EvoSkills) are also compatible with other CLI coding agents.
|
||||||
|
|
||||||
## 🔥 News
|
## 🔥 News
|
||||||
|
- **[25 Mar 2026]** 🥇 Ranked #1 on [AstaBench Code & Execution](https://huggingface.co/spaces/allenai/asta-bench-leaderboard) at submission time! [**Leaderboard**](https://allenai-asta-bench-leaderboard.hf.space/code-execution) 👈
|
||||||
- **[13 Mar 2026]** 🚀 [**EvoScientist**](https://github.com/EvoScientist/EvoScientist) officially debuts!
|
- **[13 Mar 2026]** 🚀 [**EvoScientist**](https://github.com/EvoScientist/EvoScientist) officially debuts!
|
||||||
- **[11 Mar 2026]** ⛳ Technical Report is live! [**Check it out**](https://arxiv.org/abs/2603.08127) 👈
|
- **[11 Mar 2026]** ⛳ Technical Report is live! [**Check it out**](https://arxiv.org/abs/2603.08127) 👈
|
||||||
- **[06 Mar 2026]** 🥇 Ranked #1 on [DeepResearch Bench II](https://agentresearchlab.com/benchmarks/deepresearch-bench-ii/index.html#leaderboard) at submission time! [**Leaderboard**](https://agentresearchlab.com/benchmarks/deepresearch-bench-ii/index.html#leaderboard) 👈
|
- **[06 Mar 2026]** 🥇 Ranked #1 on [DeepResearch Bench II](https://agentresearchlab.com/benchmarks/deepresearch-bench-ii/index.html#leaderboard) at submission time! [**Leaderboard**](https://agentresearchlab.com/benchmarks/deepresearch-bench-ii/index.html#leaderboard) 👈
|
||||||
|
|||||||
+9
-3
@@ -53,21 +53,26 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="33%">
|
<td align="center" valign="top" width="25%">
|
||||||
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/ICAIS_awards.JPG" height="180" alt="ICAIS 2025 Awards"/>
|
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/ICAIS_awards.JPG" height="180" alt="ICAIS 2025 Awards"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>Best Paper & Appraisal Award</b></sub>
|
<sub><b>Best Paper & Appraisal Award</b></sub>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="top" width="33%">
|
<td align="center" valign="top" width="25%">
|
||||||
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/ICAIS_best_paper.png" height="180" alt="Best Paper"/>
|
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/ICAIS_best_paper.png" height="180" alt="Best Paper"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>AI-Generated Best Paper</b></sub>
|
<sub><b>AI-Generated Best Paper</b></sub>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="top" width="33%">
|
<td align="center" valign="top" width="25%">
|
||||||
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/deepresearch_bench_2.JPG" height="180" alt="DeepResearch Bench II #1"/>
|
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/deepresearch_bench_2.JPG" height="180" alt="DeepResearch Bench II #1"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>DeepResearch Bench II 第一名</b></sub>
|
<sub><b>DeepResearch Bench II 第一名</b></sub>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="top" width="25%">
|
||||||
|
<img src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/asta_bench_code.png" height="180" alt="AstaBench Code & Execution #1"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>AstaBench 代码与执行榜 第一名</b></sub>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -106,6 +111,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
|
|||||||
|
|
||||||
## 🔥 动态
|
## 🔥 动态
|
||||||
|
|
||||||
|
- **[2026 年 3 月 25 日]** 🥇 提交时在 [AstaBench 代码与执行](https://huggingface.co/spaces/allenai/asta-bench-leaderboard) 排名第一 👈
|
||||||
- **[2026 年 3 月 13 日]** 🚀 [**EvoScientist**](https://github.com/EvoScientist/EvoScientist) 正式亮相!
|
- **[2026 年 3 月 13 日]** 🚀 [**EvoScientist**](https://github.com/EvoScientist/EvoScientist) 正式亮相!
|
||||||
- **[2026 年 3 月 11 日]** ⛳ 技术报告已上线 👈
|
- **[2026 年 3 月 11 日]** ⛳ 技术报告已上线 👈
|
||||||
- **[2026 年 3 月 6 日]** 🥇 提交时在 [DeepResearch Bench II](https://agentresearchlab.com/benchmarks/deepresearch-bench-ii/index.html#leaderboard) 排名第一 👈
|
- **[2026 年 3 月 6 日]** 🥇 提交时在 [DeepResearch Bench II](https://agentresearchlab.com/benchmarks/deepresearch-bench-ii/index.html#leaderboard) 排名第一 👈
|
||||||
|
|||||||
+59
-22
@@ -20,47 +20,74 @@ class TestParseFileMentions:
|
|||||||
def test_single_mention(self, tmp_path: Path) -> None:
|
def test_single_mention(self, tmp_path: Path) -> None:
|
||||||
f = tmp_path / "paper.tex"
|
f = tmp_path / "paper.tex"
|
||||||
f.write_text("hello")
|
f.write_text("hello")
|
||||||
result = parse_file_mentions(f"read @{f}", cwd=tmp_path)
|
files, _ = parse_file_mentions(f"read @{f}", cwd=tmp_path)
|
||||||
assert result == [f.resolve()]
|
assert files == [f.resolve()]
|
||||||
|
|
||||||
def test_relative_mention(self, tmp_path: Path) -> None:
|
def test_relative_mention(self, tmp_path: Path) -> None:
|
||||||
f = tmp_path / "notes.md"
|
f = tmp_path / "notes.md"
|
||||||
f.write_text("world")
|
f.write_text("world")
|
||||||
result = parse_file_mentions("check @notes.md", cwd=tmp_path)
|
files, _ = parse_file_mentions("check @notes.md", cwd=tmp_path)
|
||||||
assert result == [f.resolve()]
|
assert files == [f.resolve()]
|
||||||
|
|
||||||
def test_multiple_mentions(self, tmp_path: Path) -> None:
|
def test_multiple_mentions(self, tmp_path: Path) -> None:
|
||||||
a = tmp_path / "a.py"
|
a = tmp_path / "a.py"
|
||||||
b = tmp_path / "b.py"
|
b = tmp_path / "b.py"
|
||||||
a.write_text("a")
|
a.write_text("a")
|
||||||
b.write_text("b")
|
b.write_text("b")
|
||||||
result = parse_file_mentions(f"diff @{a} @{b}", cwd=tmp_path)
|
files, _ = parse_file_mentions(f"diff @{a} @{b}", cwd=tmp_path)
|
||||||
assert set(result) == {a.resolve(), b.resolve()}
|
assert set(files) == {a.resolve(), b.resolve()}
|
||||||
|
|
||||||
def test_missing_file_skipped(self, tmp_path: Path) -> None:
|
def test_missing_file_skipped(self, tmp_path: Path) -> None:
|
||||||
result = parse_file_mentions("@nonexistent.txt", cwd=tmp_path)
|
files, warnings = parse_file_mentions("@nonexistent.txt", cwd=tmp_path)
|
||||||
assert result == []
|
assert files == []
|
||||||
|
assert any("not found" in w for w in warnings)
|
||||||
|
|
||||||
def test_email_address_ignored(self, tmp_path: Path) -> None:
|
def test_email_address_ignored(self, tmp_path: Path) -> None:
|
||||||
result = parse_file_mentions("send to user@example.com", cwd=tmp_path)
|
files, warnings = parse_file_mentions("send to user@example.com", cwd=tmp_path)
|
||||||
assert result == []
|
assert files == []
|
||||||
|
assert warnings == []
|
||||||
|
|
||||||
def test_directory_excluded(self, tmp_path: Path) -> None:
|
def test_directory_excluded(self, tmp_path: Path) -> None:
|
||||||
d = tmp_path / "subdir"
|
d = tmp_path / "subdir"
|
||||||
d.mkdir()
|
d.mkdir()
|
||||||
result = parse_file_mentions(f"@{d}", cwd=tmp_path)
|
files, _ = parse_file_mentions(f"@{d}", cwd=tmp_path)
|
||||||
assert result == []
|
assert files == []
|
||||||
|
|
||||||
def test_bare_at_sign_ignored(self, tmp_path: Path) -> None:
|
def test_bare_at_sign_ignored(self, tmp_path: Path) -> None:
|
||||||
result = parse_file_mentions("hello @ world", cwd=tmp_path)
|
files, warnings = parse_file_mentions("hello @ world", cwd=tmp_path)
|
||||||
assert result == []
|
assert files == []
|
||||||
|
assert warnings == []
|
||||||
|
|
||||||
def test_tilde_expansion(self, tmp_path: Path, monkeypatch) -> None:
|
def test_tilde_expansion(self, tmp_path: Path, monkeypatch) -> None:
|
||||||
monkeypatch.setenv("HOME", str(tmp_path))
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
f = tmp_path / "file.txt"
|
f = tmp_path / "file.txt"
|
||||||
f.write_text("x")
|
f.write_text("x")
|
||||||
result = parse_file_mentions("@~/file.txt", cwd=tmp_path)
|
files, _ = parse_file_mentions("@~/file.txt", cwd=tmp_path)
|
||||||
assert result == [f.resolve()]
|
assert files == [f.resolve()]
|
||||||
|
|
||||||
|
def test_duplicate_mention_deduplicated(self, tmp_path: Path) -> None:
|
||||||
|
"""Mentioning the same file twice returns it only once."""
|
||||||
|
f = tmp_path / "file.txt"
|
||||||
|
f.write_text("hello")
|
||||||
|
files, _ = parse_file_mentions(f"@{f} and again @{f}", cwd=tmp_path)
|
||||||
|
assert files == [f.resolve()]
|
||||||
|
|
||||||
|
def test_outside_workspace_warns(self, tmp_path: Path) -> None:
|
||||||
|
"""Files outside the workspace root trigger a warning but are still returned."""
|
||||||
|
outside = tmp_path.parent / "secret.txt"
|
||||||
|
outside.write_text("secret")
|
||||||
|
workspace = tmp_path / "workspace"
|
||||||
|
workspace.mkdir()
|
||||||
|
files, warnings = parse_file_mentions(f"@{outside}", cwd=workspace)
|
||||||
|
assert files == [outside.resolve()]
|
||||||
|
assert any("outside the workspace" in w for w in warnings)
|
||||||
|
|
||||||
|
def test_inside_workspace_no_warning(self, tmp_path: Path) -> None:
|
||||||
|
"""Files inside the workspace do NOT produce an outside-workspace warning."""
|
||||||
|
f = tmp_path / "safe.txt"
|
||||||
|
f.write_text("data")
|
||||||
|
_, warnings = parse_file_mentions(f"@{f}", cwd=tmp_path)
|
||||||
|
assert not any("outside the workspace" in w for w in warnings)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -71,15 +98,16 @@ class TestParseFileMentions:
|
|||||||
class TestResolveFileMentions:
|
class TestResolveFileMentions:
|
||||||
def test_no_mentions_returns_original(self, tmp_path: Path) -> None:
|
def test_no_mentions_returns_original(self, tmp_path: Path) -> None:
|
||||||
text = "just a normal message"
|
text = "just a normal message"
|
||||||
original, final = resolve_file_mentions(text, str(tmp_path))
|
original, final, warnings = resolve_file_mentions(text, str(tmp_path))
|
||||||
assert original == text
|
assert original == text
|
||||||
assert final == text
|
assert final == text
|
||||||
|
assert warnings == []
|
||||||
|
|
||||||
def test_mention_appends_content(self, tmp_path: Path) -> None:
|
def test_mention_appends_content(self, tmp_path: Path) -> None:
|
||||||
f = tmp_path / "README.md"
|
f = tmp_path / "README.md"
|
||||||
f.write_text("# Hello")
|
f.write_text("# Hello")
|
||||||
text = f"summarise @{f}"
|
text = f"summarise @{f}"
|
||||||
original, final = resolve_file_mentions(text, str(tmp_path))
|
original, final, _ = resolve_file_mentions(text, str(tmp_path))
|
||||||
assert original == text
|
assert original == text
|
||||||
assert "## Referenced Files" in final
|
assert "## Referenced Files" in final
|
||||||
assert "README.md" in final
|
assert "README.md" in final
|
||||||
@@ -90,7 +118,7 @@ class TestResolveFileMentions:
|
|||||||
# Write more than 256 KB
|
# Write more than 256 KB
|
||||||
f.write_bytes(b"x" * (260 * 1024))
|
f.write_bytes(b"x" * (260 * 1024))
|
||||||
text = f"read @{f}"
|
text = f"read @{f}"
|
||||||
_, final = resolve_file_mentions(text, str(tmp_path))
|
_, final, _ = resolve_file_mentions(text, str(tmp_path))
|
||||||
assert "too large to embed" in final
|
assert "too large to embed" in final
|
||||||
assert "read_file" in final
|
assert "read_file" in final
|
||||||
|
|
||||||
@@ -99,7 +127,7 @@ class TestResolveFileMentions:
|
|||||||
b = tmp_path / "b.txt"
|
b = tmp_path / "b.txt"
|
||||||
a.write_text("AAA")
|
a.write_text("AAA")
|
||||||
b.write_text("BBB")
|
b.write_text("BBB")
|
||||||
_, final = resolve_file_mentions(f"@{a} and @{b}", str(tmp_path))
|
_, final, _ = resolve_file_mentions(f"@{a} and @{b}", str(tmp_path))
|
||||||
assert "AAA" in final
|
assert "AAA" in final
|
||||||
assert "BBB" in final
|
assert "BBB" in final
|
||||||
|
|
||||||
@@ -107,16 +135,25 @@ class TestResolveFileMentions:
|
|||||||
f = tmp_path / "f.txt"
|
f = tmp_path / "f.txt"
|
||||||
f.write_text("content")
|
f.write_text("content")
|
||||||
text = f"look at @{f} please"
|
text = f"look at @{f} please"
|
||||||
_, final = resolve_file_mentions(text, str(tmp_path))
|
_, final, _ = resolve_file_mentions(text, str(tmp_path))
|
||||||
assert final.startswith(text)
|
assert final.startswith(text)
|
||||||
|
|
||||||
def test_none_workspace_uses_cwd(self, tmp_path: Path, monkeypatch) -> None:
|
def test_none_workspace_uses_cwd(self, tmp_path: Path, monkeypatch) -> None:
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
f = tmp_path / "local.txt"
|
f = tmp_path / "local.txt"
|
||||||
f.write_text("local")
|
f.write_text("local")
|
||||||
_, final = resolve_file_mentions("@local.txt", None)
|
_, final, _ = resolve_file_mentions("@local.txt", None)
|
||||||
assert "local" in final
|
assert "local" in final
|
||||||
|
|
||||||
|
def test_warnings_returned_not_printed(self, tmp_path: Path) -> None:
|
||||||
|
"""Warnings are returned in the third element, not printed to stdout."""
|
||||||
|
outside = tmp_path.parent / "outside.txt"
|
||||||
|
outside.write_text("sensitive")
|
||||||
|
workspace = tmp_path / "workspace"
|
||||||
|
workspace.mkdir()
|
||||||
|
_, _, warnings = resolve_file_mentions(f"@{outside}", str(workspace))
|
||||||
|
assert any("outside the workspace" in w for w in warnings)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# complete_file_mention
|
# complete_file_mention
|
||||||
|
|||||||
Reference in New Issue
Block a user