mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
Some checks failed
Tests ReMe / Unit Tests - py3.12 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.13 (push) Has been cancelled
Windows Smoke / CLI smoke - py3.11 (push) Has been cancelled
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Tests ReMe / Unit Tests - py3.11 (push) Has been cancelled
* refactor: rebuild auto-fin and daily-paper cookbooks on structured-output agents Rework the auto-fin and daily-paper cookbooks to run on structured-output LLM agents instead of Claude Code agent wrappers, replace the SSH proxy with data-source mirrors, and rewrite the affected unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(auto_fin): unify JSON output serialization and writing - Extracted _write_output static method to serialize and write Pydantic models as compact JSON - Replaced inline JSON dump and write calls with _write_output usage across auto_fin steps - Added _report_path and _current_report for managing intra-day reports in AutoFinMergeStep - Updated auto_fin merge step to write output via new _write_output method - Enhanced news reading with caching in AutoFinHistoryStep - Refined returns calculation to handle events before close on non-trading days correctly feat(daily_paper): improve note path resolution and metadata handling - Introduced iter_note_metadata generator for safe Markdown frontmatter iteration - Added resolve_unique_note_path to avoid note filename conflicts on disk and in used titles - Updated analyze, collect, digest, and select steps to use centralized constants and helpers - Used utc_now_iso for consistent timestamping in metadata - Replaced direct frontmatter loads with iter_note_metadata in collect and analyze steps - Replaced hardcoded paper selection count with PAPER_COUNT constant in all relevant places - Added _MAX_SELECT_ATTEMPTS constant in select step for attempt management - Improved error messages for filename validation in daily paper title normalization feat(auto_fin): add multi-run cron schedules for intraday refinement - Defined three auto_fin cron jobs at 09:30, 11:30, and 18:00 Shanghai time for gradual report updates - Each intraday run adds evidence cumulatively instead of replacing prior output wholly - Updated daily_cookbook.yaml to register new cron schedules and remove legacy 12:00 cron refactor(auto_fin_data): clean ETF code handling and page limits - Replaced hardcoded DEFAULT_ETF_CODES with required non-empty config value "etf_codes" - Added constants for major news and fund page limits to control pagination - Improved ETF name extraction logic to handle missing fields consistently fix(auto_fin_merge): fix report retrieval and merging logic - Added support for getting current intra-day report in addition to previous day's report - Modified merge template to include prior and current report sections for better context - Adjusted report path handling to consistently use Path objects test(auto_fin): add coverage for returns calculation and report retrieval - Added test for returns when event occurs before close on non-trading day, checking next session entry - Added test for previous and current report retrieval feeding merge context with disk files - Extended test asserts for auto_fin cron schedule changes in config style(daily_paper): reorder and cleanup imports - Reorganized imports in _common.py for clarity and added missing collections.abc.Iterator import - Cleaned up commented and unused imports across daily_paper steps * feat: add configurable upstream mirror proxy * style: format auto-fin data step * fix: align cookbook mirrors and contracts --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
4.3 KiB
Python
107 lines
4.3 KiB
Python
"""arXiv validation and PDF download helpers."""
|
|
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
import aiofiles
|
|
import httpx
|
|
|
|
from .logger_utils import get_logger
|
|
|
|
ARXIV_ID_PATTERN = re.compile(r"^\d{4}\.\d{4,5}$")
|
|
ARXIV_BASE_URL = "https://arxiv.org"
|
|
|
|
|
|
class ArxivPdfClient:
|
|
"""Download validated arXiv PDFs to local files."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
client: httpx.AsyncClient | None = None,
|
|
timeout: float = 600.0,
|
|
max_bytes: int = 50 * 1024 * 1024,
|
|
logger: Any | None = None,
|
|
) -> None:
|
|
self.base_url = os.getenv("ARXIV_MIRROR_URL", "").strip().rstrip("/") or ARXIV_BASE_URL
|
|
self.client = client
|
|
self._owns_client = client is None
|
|
self.timeout, self.max_bytes = timeout, max_bytes
|
|
self.logger = logger or get_logger()
|
|
|
|
async def __aenter__(self) -> "ArxivPdfClient":
|
|
if self.client is None:
|
|
self.client = httpx.AsyncClient(
|
|
timeout=self.timeout,
|
|
follow_redirects=True,
|
|
headers={"User-Agent": "ReMe arXiv client"},
|
|
)
|
|
self.logger.info(f"[ArxivPdfClient] base_url={self.base_url}")
|
|
else:
|
|
self.logger.debug("[ArxivPdfClient] network mode=injected_client")
|
|
return self
|
|
|
|
async def __aexit__(self, _exc_type, _exc_value, _traceback) -> None:
|
|
if self._owns_client and self.client is not None:
|
|
await self.client.aclose()
|
|
self.client = None
|
|
|
|
def _require_client(self) -> httpx.AsyncClient:
|
|
if self.client is None:
|
|
raise RuntimeError("ArxivPdfClient must be used as an async context manager")
|
|
return self.client
|
|
|
|
async def download(self, arxiv_id: str, target: Path) -> Path:
|
|
"""Download one PDF atomically, reusing an existing valid target."""
|
|
client = self._require_client()
|
|
if not ARXIV_ID_PATTERN.fullmatch(arxiv_id):
|
|
raise ValueError(f"Invalid arXiv id: {arxiv_id!r}")
|
|
if target.is_file() and target.stat().st_size > 5:
|
|
with target.open("rb") as existing:
|
|
if existing.read(5) == b"%PDF-":
|
|
self.logger.debug(
|
|
f"[ArxivPdfClient] cache hit arxiv_id={arxiv_id} path={target} bytes={target.stat().st_size}",
|
|
)
|
|
return target
|
|
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
part_path = target.with_name(f".{target.name}.{uuid4().hex}.part")
|
|
size = 0
|
|
self.logger.info(
|
|
f"[ArxivPdfClient] download start arxiv_id={arxiv_id} path={target} timeout={self.timeout:g}s",
|
|
)
|
|
try:
|
|
async with client.stream("GET", f"{self.base_url}/pdf/{arxiv_id}") as response:
|
|
response.raise_for_status()
|
|
content_length = int(response.headers.get("content-length") or 0)
|
|
if content_length and content_length > self.max_bytes:
|
|
raise ValueError(f"PDF exceeds maximum size: {content_length} > {self.max_bytes}")
|
|
async with aiofiles.open(part_path, "wb") as stream:
|
|
async for chunk in response.aiter_bytes():
|
|
size += len(chunk)
|
|
if size > self.max_bytes:
|
|
raise ValueError(f"PDF exceeds maximum size: {size} > {self.max_bytes}")
|
|
await stream.write(chunk)
|
|
if size <= 5:
|
|
raise ValueError(f"Downloaded PDF is empty for {arxiv_id}")
|
|
async with aiofiles.open(part_path, "rb") as stream:
|
|
header = await stream.read(5)
|
|
if header != b"%PDF-":
|
|
raise ValueError(f"Downloaded content is not a PDF for {arxiv_id}")
|
|
os.replace(part_path, target)
|
|
self.logger.info(
|
|
f"[ArxivPdfClient] download done arxiv_id={arxiv_id} path={target} bytes={size}",
|
|
)
|
|
return target
|
|
except Exception as exc:
|
|
detail = str(exc) or "-"
|
|
self.logger.warning(
|
|
f"[ArxivPdfClient] download failed arxiv_id={arxiv_id} error={type(exc).__name__} detail={detail}",
|
|
)
|
|
raise
|
|
finally:
|
|
if part_path.exists():
|
|
part_path.unlink()
|