#!/usr/bin/env python3 """Collect a paginated, abstract-level Agent-memory research corpus from arXiv.""" from __future__ import annotations import argparse import json import re import time import urllib.error import urllib.parse import urllib.request import xml.etree.ElementTree as ET from collections import defaultdict from datetime import date from pathlib import Path ROOT = Path(__file__).resolve().parents[2] API_URL = "https://export.arxiv.org/api/query" NS = { "atom": "http://www.w3.org/2005/Atom", "opensearch": "http://a9.com/-/spec/opensearch/1.1/", } QUERIES = [ ("agent-memory", 'all:"agent memory" OR all:"memory agent" OR all:"memory agents"'), ( "long-term-memory", '(all:"long-term memory" OR all:"persistent memory") AND ' '(all:"LLM agent" OR all:"language agent" OR all:"large language model agent")', ), ( "episodic-memory", 'all:"episodic memory" AND (all:"LLM" OR all:"large language model") AND all:"agent"', ), ( "procedural-memory", '(all:"procedural memory" OR all:"skill memory") AND ' '(all:"LLM agent" OR all:"language agent")', ), ( "memory-evaluation", '(all:"memory benchmark" OR all:"memory evaluation") AND ' '(all:"LLM agent" OR all:"language agent" OR all:"agent memory")', ), ] def normalize(value: str) -> str: return re.sub(r"\s+", " ", value).strip() def arxiv_id(raw: str) -> str: return raw.rsplit("/", 1)[-1].split("v", 1)[0] def date_query(query: str, start: str, end: str) -> str: return ( f"({query}) AND submittedDate:[{start.replace('-', '')}0000 " f"TO {end.replace('-', '')}2359]" ) def fetch_page( query: str, start_date: str, end_date: str, offset: int, page_size: int, retries: int, timeout: int, ) -> bytes: params = { "search_query": date_query(query, start_date, end_date), "start": offset, "max_results": page_size, "sortBy": "submittedDate", "sortOrder": "ascending", } request = urllib.request.Request( f"{API_URL}?{urllib.parse.urlencode(params)}", headers={"User-Agent": "agent-kb-research/0.1"}, ) for attempt in range(retries + 1): try: with urllib.request.urlopen(request, timeout=timeout) as response: return response.read() except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError): if attempt >= retries: raise time.sleep(2 ** attempt) raise RuntimeError("unreachable retry state") def parse_page(payload: bytes) -> tuple[int, list[dict]]: root = ET.fromstring(payload) total = int(root.findtext("opensearch:totalResults", default="0", namespaces=NS)) records = [] for entry in root.findall("atom:entry", NS): paper_id = arxiv_id(entry.findtext("atom:id", default="", namespaces=NS)) records.append( { "arxiv_id": paper_id, "title": normalize(entry.findtext("atom:title", default="", namespaces=NS)), "abstract": normalize(entry.findtext("atom:summary", default="", namespaces=NS)), "published": entry.findtext("atom:published", default="", namespaces=NS)[:10], "updated": entry.findtext("atom:updated", default="", namespaces=NS)[:10], "authors": [ normalize(author.findtext("atom:name", default="", namespaces=NS)) for author in entry.findall("atom:author", NS) ], "categories": [ node.attrib["term"] for node in entry.findall("atom:category", NS) if node.attrib.get("term") ], "url": f"https://arxiv.org/abs/{paper_id}", "pdf_url": f"https://arxiv.org/pdf/{paper_id}", } ) return total, records def write_json(path: Path, payload: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) tmp = path.with_suffix(path.suffix + ".tmp") tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") tmp.replace(path) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--from-date", default="2025-01-01") parser.add_argument("--to-date", default=date.today().isoformat()) parser.add_argument("--page-size", type=int, default=100) parser.add_argument("--max-per-query", type=int, default=2000) parser.add_argument("--sleep", type=float, default=1.0) parser.add_argument("--retries", type=int, default=4) parser.add_argument("--timeout", type=int, default=45) parser.add_argument( "--output", type=Path, default=ROOT / "data" / "research" / "agent-memory-expanded.json", ) args = parser.parse_args() records: dict[str, dict] = {} matched_queries: defaultdict[str, set[str]] = defaultdict(set) query_totals = {} for name, query in QUERIES: offset = 0 while offset < args.max_per_query: payload = fetch_page( query, args.from_date, args.to_date, offset, args.page_size, args.retries, args.timeout, ) total, page = parse_page(payload) query_totals[name] = total for record in page: records[record["arxiv_id"]] = record matched_queries[record["arxiv_id"]].add(name) offset += len(page) print(f"{name}: {min(offset, total)}/{total}", flush=True) if not page or offset >= total: break time.sleep(args.sleep) time.sleep(args.sleep) papers = [ {**record, "matched_queries": sorted(matched_queries[paper_id])} for paper_id, record in records.items() ] papers.sort(key=lambda item: (item["published"], item["arxiv_id"])) output = { "generated_at": date.today().isoformat(), "from_date": args.from_date, "to_date": args.to_date, "query_totals": query_totals, "unique_papers": len(papers), "papers": papers, } write_json(args.output, output) print(json.dumps({"query_totals": query_totals, "unique_papers": len(papers)}, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())