195 lines
6.4 KiB
Python
195 lines
6.4 KiB
Python
#!/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())
|