mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-15 09:11:24 +00:00
fix(daily-paper): validate cleaned Markdown within bounded retry
This commit is contained in:
parent
8543f878de
commit
488c803db1
4 changed files with 54 additions and 14 deletions
|
|
@ -10,11 +10,10 @@ from .base import (
|
|||
PAPER_COUNT,
|
||||
DailyPaperStep,
|
||||
iter_note_metadata,
|
||||
markdown_output,
|
||||
normalize_chinese_title,
|
||||
replace_surrogates,
|
||||
reserve_unique_note_path,
|
||||
strip_frontmatter,
|
||||
structured_output,
|
||||
utc_now_iso,
|
||||
write_markdown,
|
||||
)
|
||||
|
|
@ -119,7 +118,7 @@ class DailyPaperAnalyzeStep(DailyPaperStep):
|
|||
for attempt in range(2):
|
||||
try:
|
||||
result = await self.agent_wrapper.reply(prompt, output_schema=DailyPaperMarkdownOutput)
|
||||
output = structured_output(result, DailyPaperMarkdownOutput)
|
||||
output = markdown_output(result)
|
||||
break
|
||||
except recoverable:
|
||||
if attempt:
|
||||
|
|
@ -141,10 +140,7 @@ class DailyPaperAnalyzeStep(DailyPaperStep):
|
|||
) as (title, note_path):
|
||||
used_titles.add(title)
|
||||
note_rel = note_path.relative_to(self.workspace_path).as_posix()
|
||||
desc = replace_surrogates(output.desc.strip())
|
||||
body = replace_surrogates(strip_frontmatter(output.body))
|
||||
if not desc or not body:
|
||||
raise ValueError(f"Agent returned an empty paper note for {paper.arxiv_id}")
|
||||
desc, body = output.desc, output.body
|
||||
await write_markdown(
|
||||
note_path,
|
||||
body,
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ from pydantic import BaseModel
|
|||
from reme.steps import BaseStep
|
||||
from reme.steps.file_io import get_path_lock, validate_filename_component
|
||||
|
||||
from .schema import DailyPaperMarkdownOutput
|
||||
|
||||
# Number of papers selected, analyzed, and digested each run. Shared across steps.
|
||||
PAPER_COUNT = 3
|
||||
_STATE_PREFIX = "daily_paper_"
|
||||
_FRONTMATTER_PATTERN = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL)
|
||||
_FRONTMATTER_PATTERN = re.compile(r"^---\s*\n.*?\n---(?:\s*\n|$)", re.DOTALL)
|
||||
_MARKDOWN_HEADING_PATTERN = re.compile(r"^#+\s*")
|
||||
_UNSAFE_FILENAME_CHARS = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
||||
_CHINESE_PATTERN = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]")
|
||||
|
|
@ -57,6 +59,16 @@ def _replace_surrogates_recursive(value: Any) -> Any:
|
|||
return value
|
||||
|
||||
|
||||
def markdown_output(result: dict[str, Any]) -> DailyPaperMarkdownOutput:
|
||||
"""Clean and validate usable Markdown before callers persist or retry it."""
|
||||
output = structured_output(result, DailyPaperMarkdownOutput)
|
||||
desc = replace_surrogates(output.desc.strip())
|
||||
body = replace_surrogates(strip_frontmatter(output.body))
|
||||
if not desc or not body:
|
||||
raise ValueError("Agent returned an empty Markdown description or body")
|
||||
return output.model_copy(update={"desc": desc, "body": body})
|
||||
|
||||
|
||||
def normalize_chinese_title(raw: str, fallback: str) -> str:
|
||||
"""Return one safe Chinese title that can also be used as the filename stem."""
|
||||
title = replace_surrogates(str(raw or "").strip())
|
||||
|
|
|
|||
|
|
@ -11,10 +11,9 @@ from reme.steps.file_io import refresh_day_index
|
|||
from .base import (
|
||||
PAPER_COUNT,
|
||||
DailyPaperStep,
|
||||
markdown_output,
|
||||
normalize_chinese_title,
|
||||
reserve_unique_note_path,
|
||||
strip_frontmatter,
|
||||
structured_output,
|
||||
utc_now_iso,
|
||||
write_markdown,
|
||||
)
|
||||
|
|
@ -136,10 +135,8 @@ class DailyPaperDigestStep(DailyPaperStep):
|
|||
},
|
||||
)
|
||||
self.logger.info(f"[{self.name}] agent done notes={len(analyses)}")
|
||||
output = structured_output(result, DailyPaperMarkdownOutput)
|
||||
body = strip_frontmatter(output.body)
|
||||
if not output.desc.strip() or not body:
|
||||
raise ValueError("Agent returned an empty daily paper brief")
|
||||
output = markdown_output(result)
|
||||
body = output.body
|
||||
|
||||
day = run_day.isoformat()
|
||||
title = normalize_chinese_title(output.title, f"每日论文简报-{day}")
|
||||
|
|
|
|||
|
|
@ -1213,3 +1213,38 @@ async def test_digest_failure_boundary_preserves_cancellation(tmp_path):
|
|||
step._generate_digest = AsyncMock(side_effect=asyncio.CancelledError) # pylint: disable=protected-access
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await step(RuntimeContext())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"invalid",
|
||||
[
|
||||
{"title": "论文", "desc": " ", "body": "content"},
|
||||
{"title": "论文", "desc": "summary", "body": "\n "},
|
||||
{"title": "论文", "desc": "summary", "body": "---\ntitle: generated\n---\n"},
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("recover", [True, False])
|
||||
async def test_blank_note_retries_once_before_writing(tmp_path, monkeypatch, invalid, recover):
|
||||
"""Blank or frontmatter-only content uses the same bounded shorter-note recovery."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
valid = {"title": "论文", "desc": " summary ", "body": "---\ntitle: model\n---\nEvidence"}
|
||||
agent = _QueuedAgentWrapper([invalid, valid if recover else invalid])
|
||||
step = DailyPaperAnalyzeStep(app_context=ApplicationContext(workspace_dir=str(tmp_path)), agent_wrapper=agent)
|
||||
step.context = RuntimeContext(daily_paper_run_date="2026-07-21")
|
||||
monkeypatch.setattr(step, "_extract_pdf_text_sync", lambda *_: ("evidence", 1, False))
|
||||
downloader = SimpleNamespace(download=AsyncMock())
|
||||
paper = _paper("2607.10001")
|
||||
pick = PaperPick(arxiv_id=paper.arxiv_id, reasoning="test")
|
||||
if recover:
|
||||
result = await step._analyze_one(downloader, paper, pick, set()) # pylint: disable=protected-access
|
||||
assert result.desc == "summary" and result.body == "Evidence"
|
||||
assert (tmp_path / result.note_path).is_file()
|
||||
else:
|
||||
with pytest.raises(ValueError, match="empty Markdown"):
|
||||
await step._analyze_one(downloader, paper, pick, set()) # pylint: disable=protected-access
|
||||
assert not list(tmp_path.rglob("*.md"))
|
||||
assert len(agent.calls) == 2
|
||||
assert step.prompt_format("analyze_short_retry") in agent.calls[1]["inputs"]
|
||||
downloader.download.assert_awaited_once()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue