Audit recent Agent memory research

This commit is contained in:
wuyang
2026-07-10 18:52:00 +08:00
parent 36c631e026
commit ab758e774e
17 changed files with 42663 additions and 3 deletions
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""Build auditable landscape counts and evidence candidates for Agent Memory."""
from __future__ import annotations
import argparse
import csv
import json
import re
from collections import Counter, defaultdict
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
THEMES = {
"retrieval_access": r"retriev|recall|search|query|evidence access|memory navigation",
"representation_organization": r"knowledge graph|graph memory|hierarch|structure|representation|index|database|topic document|ontology",
"update_forgetting": r"update|forget|consolidat|supersed|stale|conflict|write path|write-path|memory admission|retention",
"execution_state": r"execution state|task state|workflow state|belief state|long-horizon|trajectory|subgoal|planning",
"experience_skill_learning": r"experience|procedural|skill|reflect|self-evolv|test-time learn|continual learn|reinforcement learn|\brl\b",
"evaluation_diagnosis": r"benchmark|evaluat|diagnos|metric|ablation|empirical study|controlled",
"security_privacy_trust": r"attack|poison|security|privacy|trust|provenance|govern|leak|integrity|adversarial",
"multi_agent_shared": r"multi-agent|multiagent|shared memory|collective memory|collaborative memory",
"multimodal_embodied_gui": r"multimodal|visual|video|robot|embodied|gui|mobile|web agent|navigation",
"systems_efficiency": r"token|latency|cost|efficient|compression|serving|storage|throughput|budget",
}
# Title-only labels describe the paper's declared contribution more conservatively
# than abstract keywords such as "evaluate", which occur in almost every paper.
TITLE_THEMES = {
"benchmark_diagnosis": r"benchmark|evaluat|diagnos|empirical|survey|taxonomy|anatomy|rethinking|what .* need|how far|study of|analysis",
"retrieval_representation": r"retriev|recall|search|graph|hierarch|structure|index|database|ontology|organization|representation|rag\b|topic document",
"update_forgetting": r"update|forget|consolidat|stale|supersed|decay|write memory|not to write|promotion|temporal conflict|belief revision|lifecycle",
"experience_skill_policy": r"experience|skill|procedural|learn|evolv|adapt|reinforcement|\brl\b|credit assignment|distill",
"execution_multimodal": r"execution state|task-driving state|task state|long-horizon|gui|embodied|robot|navigation|web agent|multimodal|video|spatial",
"security_governance": r"attack|poison|security|privacy|trust|govern|provenance|integrity|leak|hijack|vulnerab|unlearn|false promotion|contamination",
"systems_efficiency": r"efficient|cost|token|latency|serving|on-device|edge|compress|scalable|storage|runtime|budget|wire format",
"personal_shared": r"personali|user-aware|user-centric|multi-agent|multiagent|shared memory|collaborative|collective",
}
WINDOWS = {
"previous_six_months": ("2025-07-09", "2026-01-10"),
"recent_six_months": ("2026-01-10", "2026-07-11"),
}
CENTRAL_TITLE = re.compile(r"memory|memori|remember|forget|stateful", re.I)
AGENT_CONTEXT = re.compile(r"agent|large language model|\bllm\b|foundation model", re.I)
HARDWARE_ONLY = re.compile(
r"gpu memory|memory bandwidth|memory footprint|memory-efficient|memory efficient|bounded memory agents on",
re.I,
)
NUMERIC_RESULT = re.compile(r"(?:\d+(?:\.\d+)?\s*%|\+?\d+(?:\.\d+)?\s*(?:pp|points?|×|x))", re.I)
def period(published: str) -> str:
if published < "2025-07-01":
return "2025-H1"
if published < "2026-02-01":
return "baseline-2025H2-Jan2026"
if published < "2026-04-01":
return "transition-2026-Feb-Mar"
return "recent-2026-Apr-Jul"
def central_by_text(record: dict) -> bool:
title = record["title"]
abstract = record["abstract"]
return bool(
CENTRAL_TITLE.search(title)
and AGENT_CONTEXT.search(f"{title} {abstract}")
and not HARDWARE_ONLY.search(title)
)
def theme_labels(record: dict) -> list[str]:
text = f"{record['title']} {record['abstract']}".lower()
return [name for name, pattern in THEMES.items() if re.search(pattern, text, re.I)]
def title_theme_labels(record: dict) -> list[str]:
title = record["title"].lower()
return [name for name, pattern in TITLE_THEMES.items() if re.search(pattern, title, re.I)]
def window_name(published: str) -> str | None:
for name, (start, end) in WINDOWS.items():
if start <= published < end:
return name
return None
def evidence_score(record: dict) -> int:
text = record["abstract"].lower()
score = 0
score += 2 if re.search(r"benchmark|evaluate|evaluation|experiment", text) else 0
score += 2 if re.search(r"baseline|compare|outperform|versus|\bvs\.?\b", text) else 0
score += 2 if NUMERIC_RESULT.search(text) else 0
score += 1 if re.search(r"ablation|controlled|factorial|paired|significan", text) else 0
score += 1 if re.search(r"code is|github|we release|open-source", text) else 0
score += 1 if re.search(r"limitation|does not|not always|far from|fails?", text) else 0
return score
def write_json(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--input",
type=Path,
default=ROOT / "data" / "research" / "agent-memory-expanded.json",
)
parser.add_argument(
"--screen",
type=Path,
default=ROOT / "data" / "research" / "agent-memory-screened.json",
)
parser.add_argument(
"--output",
type=Path,
default=ROOT / "data" / "research" / "memory-landscape.json",
)
parser.add_argument(
"--csv",
type=Path,
default=ROOT / "data" / "research" / "memory-paper-ledger.csv",
)
args = parser.parse_args()
source = json.loads(args.input.read_text(encoding="utf-8"))
screen = {}
if args.screen.exists():
screened = json.loads(args.screen.read_text(encoding="utf-8"))
screen = {item["arxiv_id"]: item.get("screen") or {} for item in screened.get("papers", [])}
records = []
for paper in source["papers"]:
screened = screen.get(paper["arxiv_id"])
records.append(
{
**paper,
"period": period(paper["published"]),
"central_by_title": central_by_text(paper),
"themes": theme_labels(paper),
"title_themes": title_theme_labels(paper),
"evidence_score": evidence_score(paper),
"screen": screened,
}
)
period_counts: defaultdict[str, Counter] = defaultdict(Counter)
theme_counts: defaultdict[str, Counter] = defaultdict(Counter)
window_counts: defaultdict[str, Counter] = defaultdict(Counter)
title_theme_counts: defaultdict[str, Counter] = defaultdict(Counter)
screen_counts: defaultdict[str, Counter] = defaultdict(Counter)
for record in records:
bucket = record["period"]
period_counts[bucket]["candidates"] += 1
if record["central_by_title"]:
period_counts[bucket]["title_central"] += 1
relevance = (record.get("screen") or {}).get("relevance")
if relevance:
period_counts[bucket][f"screen_{relevance}"] += 1
for theme in record["themes"]:
if record["central_by_title"]:
theme_counts[bucket][theme] += 1
window = window_name(record["published"])
if window:
window_counts[window]["candidates"] += 1
if record["central_by_title"]:
window_counts[window]["title_central"] += 1
for theme in record["title_themes"]:
title_theme_counts[window][theme] += 1
screened = record.get("screen") or {}
if screened:
screen_counts["relevance"][screened.get("relevance", "unknown")] += 1
screen_counts["problem"][screened.get("problem", "unknown")] += 1
screen_counts["source"][screened.get("source", "unknown")] += 1
top_evidence = {}
for theme in THEMES:
candidates = [
record
for record in records
if record["central_by_title"] and theme in record["themes"]
]
candidates.sort(key=lambda item: (item["evidence_score"], item["published"]), reverse=True)
top_evidence[theme] = [
{
"arxiv_id": item["arxiv_id"],
"published": item["published"],
"title": item["title"],
"score": item["evidence_score"],
"url": item["url"],
}
for item in candidates[:20]
]
output = {
"generated_at": date.today().isoformat(),
"source_candidates": len(records),
"title_central": sum(record["central_by_title"] for record in records),
"screened": len(screen),
"windows": {
name: {
"from_inclusive": start,
"to_exclusive": end,
**dict(window_counts[name]),
"title_theme_counts": dict(title_theme_counts[name]),
}
for name, (start, end) in WINDOWS.items()
},
"period_counts": {key: dict(value) for key, value in period_counts.items()},
"theme_counts_title_central": {key: dict(value) for key, value in theme_counts.items()},
"screen_counts": {key: dict(value) for key, value in screen_counts.items()},
"evidence_score_distribution": dict(
Counter(record["evidence_score"] for record in records if record["central_by_title"])
),
"top_evidence_candidates": top_evidence,
}
write_json(args.output, output)
args.csv.parent.mkdir(parents=True, exist_ok=True)
with args.csv.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(
handle,
fieldnames=[
"arxiv_id",
"published",
"period",
"title",
"url",
"title_central",
"screen_relevance",
"screen_problem",
"screen_source",
"themes",
"title_themes",
"evidence_score",
"matched_queries",
],
)
writer.writeheader()
for record in records:
screened = record.get("screen") or {}
writer.writerow(
{
"arxiv_id": record["arxiv_id"],
"published": record["published"],
"period": record["period"],
"title": record["title"],
"url": record["url"],
"title_central": record["central_by_title"],
"screen_relevance": screened.get("relevance", ""),
"screen_problem": screened.get("problem", ""),
"screen_source": screened.get("source", ""),
"themes": ";".join(record["themes"]),
"title_themes": ";".join(record["title_themes"]),
"evidence_score": record["evidence_score"],
"matched_queries": ";".join(record["matched_queries"]),
}
)
print(
json.dumps(
{
"source_candidates": output["source_candidates"],
"title_central": output["title_central"],
"screened": output["screened"],
"windows": output["windows"],
"screen_counts": output["screen_counts"],
},
ensure_ascii=False,
indent=2,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Classify an Agent-memory abstract corpus into a research evidence ledger."""
from __future__ import annotations
import argparse
import json
import time
import urllib.error
import urllib.request
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
DEFAULT_OLLAMA_URL = "http://192.168.1.10:11434"
SYSTEM_INSTRUCTIONS = """You are building an evidence ledger for a research review of Agent Memory.
Classify each paper from title and abstract only. Be conservative. Do not infer unreported baselines,
results, limitations, or artifacts. A paper is core only when memory itself is the main research object;
supporting means memory is a substantial mechanism or measured failure; peripheral means memory is
incidental or only an implementation detail.
Return one JSON object with a `papers` array in the same order. Each item must contain:
- arxiv_id: exact input id
- relevance: core | supporting | peripheral
- relevance_reason: <= 18 words
- primary_problem: one of retention_context, retrieval_access, organization_representation,
update_consolidation_forgetting, execution_state, experience_skill_learning,
evaluation_measurement, security_trust_privacy, multi_agent_shared_memory,
systems_efficiency, domain_application, theory_foundations, other
- secondary_problems: up to 3 values from the same enum
- research_role: method | benchmark | diagnostic | theory | survey | security | application | infrastructure
- memory_object: facts | events | trajectories | skills | execution_state | model_internal |
shared_state | multimodal | mixed | unclear
- temporal_scope: in_episode | cross_episode | both | unclear
- claimed_gap: one short sentence, or empty
- mechanism: one short sentence, or empty
- benchmarks: explicit names only, as an array
- baselines: explicit names or baseline families only, as an array
- reported_results: explicit numeric claims only, as an array of short strings
- evidence_design: none | case_study | benchmark_comparison | controlled_ablation |
formal_analysis | survey_synthesis
- evidence_strength: none | weak | moderate | strong
- failure_or_boundary: only a boundary explicitly visible in the abstract, or empty
Use compact English. Output JSON only."""
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 ollama_generate(url: str, model: str, prompt: str, timeout: int, retries: int) -> dict:
payload = {
"model": model,
"prompt": prompt,
"stream": False,
"format": "json",
"options": {"temperature": 0, "num_predict": 6000},
}
request = urllib.request.Request(
f"{url.rstrip('/')}/api/generate",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
for attempt in range(retries + 1):
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
body = json.loads(response.read().decode("utf-8"))
return json.loads(body["response"])
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError):
if attempt >= retries:
raise
time.sleep(2 ** attempt)
raise RuntimeError("unreachable retry state")
def classify_batch(
records: list[dict], url: str, model: str, timeout: int, retries: int
) -> list[dict]:
compact = [
{"arxiv_id": item["arxiv_id"], "title": item["title"], "abstract": item["abstract"]}
for item in records
]
prompt = f"{SYSTEM_INSTRUCTIONS}\n\nINPUT:\n{json.dumps(compact, ensure_ascii=False)}"
result = ollama_generate(url, model, prompt, timeout=timeout, retries=retries)
classified = result.get("papers") if isinstance(result, dict) else None
if not isinstance(classified, list):
raise ValueError("model response lacks papers array")
expected = [item["arxiv_id"] for item in records]
received = [str(item.get("arxiv_id")) for item in classified]
if received != expected:
raise ValueError(f"model returned unexpected ids: expected={expected}, received={received}")
return classified
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--input", type=Path, default=ROOT / "data" / "research" / "memory-corpus.json"
)
parser.add_argument(
"--output", type=Path, default=ROOT / "data" / "research" / "memory-classified.json"
)
parser.add_argument("--ollama-url", default=DEFAULT_OLLAMA_URL)
parser.add_argument("--model", default="ChatGPT-5.6:Sol")
parser.add_argument("--batch-size", type=int, default=12)
parser.add_argument("--timeout", type=int, default=300)
parser.add_argument("--retries", type=int, default=2)
parser.add_argument("--limit", type=int)
parser.add_argument("--refresh", action="store_true")
args = parser.parse_args()
args.input = args.input.resolve()
args.output = args.output.resolve()
source = json.loads(args.input.read_text(encoding="utf-8"))
records = source["papers"][: args.limit] if args.limit else source["papers"]
completed: dict[str, dict] = {}
if args.output.exists() and not args.refresh:
old = json.loads(args.output.read_text(encoding="utf-8"))
completed = {item["arxiv_id"]: item for item in old.get("papers", [])}
pending = [item for item in records if item["arxiv_id"] not in completed]
for offset in range(0, len(pending), args.batch_size):
batch = pending[offset : offset + args.batch_size]
try:
classified = classify_batch(
batch,
url=args.ollama_url,
model=args.model,
timeout=args.timeout,
retries=args.retries,
)
except Exception:
if len(batch) == 1:
raise
classified = []
for record in batch:
classified.extend(
classify_batch(
[record],
url=args.ollama_url,
model=args.model,
timeout=args.timeout,
retries=args.retries,
)
)
for source_record, classification in zip(batch, classified, strict=True):
completed[source_record["arxiv_id"]] = {**source_record, "analysis": classification}
ordered = [completed[item["arxiv_id"]] for item in records if item["arxiv_id"] in completed]
write_json(
args.output,
{
"generated_at": date.today().isoformat(),
"model": args.model,
"source": str(args.input.relative_to(ROOT)) if args.input.is_relative_to(ROOT) else str(args.input),
"total": len(records),
"completed": len(ordered),
"papers": ordered,
},
)
print(f"classified {len(ordered)}/{len(records)}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Build an abstract-level research corpus for one local paper topic."""
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 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"}
ARXIV_ID_RE = re.compile(r"arxiv\.org/(?:abs|html|pdf)/([0-9]{4}\.[0-9]{4,5})")
def normalize(value: str) -> str:
return re.sub(r"\s+", " ", value).strip()
def extract_arxiv_id(url: str) -> str | None:
match = ARXIV_ID_RE.search(url or "")
return match.group(1) if match else None
def parse_feed(payload: bytes) -> dict[str, dict]:
root = ET.fromstring(payload)
records: dict[str, dict] = {}
for entry in root.findall("atom:entry", NS):
raw_id = entry.findtext("atom:id", default="", namespaces=NS)
paper_id = raw_id.rsplit("/", 1)[-1].split("v", 1)[0]
records[paper_id] = {
"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 records
def fetch_batch(ids: list[str], retries: int, timeout: int) -> dict[str, dict]:
query = urllib.parse.urlencode({"id_list": ",".join(ids), "max_results": len(ids)})
request = urllib.request.Request(
f"{API_URL}?{query}",
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 parse_feed(response.read())
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError):
if attempt >= retries:
raise
time.sleep(2 ** attempt)
raise RuntimeError("unreachable retry state")
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("--topic", default="memory")
parser.add_argument("--index", type=Path, default=ROOT / "data" / "index.json")
parser.add_argument("--output", type=Path)
parser.add_argument("--batch-size", type=int, default=20)
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("--refresh", action="store_true")
args = parser.parse_args()
output = args.output or ROOT / "data" / "research" / f"{args.topic}-corpus.json"
index = json.loads(args.index.read_text(encoding="utf-8"))
selected: dict[str, dict] = {}
missing_ids: list[dict] = []
for item in index:
meta = item.get("meta") or {}
if item.get("collection") != "papers" or args.topic not in (meta.get("topics") or []):
continue
paper_id = extract_arxiv_id(str(meta.get("url") or ""))
if not paper_id:
missing_ids.append({"path": item.get("path"), "title": item.get("title")})
continue
selected[paper_id] = {
"path": item.get("path"),
"local_title": item.get("title"),
"local_status": meta.get("status"),
"local_topics": meta.get("topics") or [],
"collection_queries": meta.get("collection_queries"),
"collection_score": meta.get("collection_score"),
}
cached: dict[str, dict] = {}
if output.exists() and not args.refresh:
old = json.loads(output.read_text(encoding="utf-8"))
cached = {record["arxiv_id"]: record for record in old.get("papers", [])}
fetched: dict[str, dict] = {}
ids_to_fetch = [paper_id for paper_id in selected if paper_id not in cached]
for offset in range(0, len(ids_to_fetch), args.batch_size):
batch = ids_to_fetch[offset : offset + args.batch_size]
fetched.update(fetch_batch(batch, retries=args.retries, timeout=args.timeout))
print(f"fetched {min(offset + len(batch), len(ids_to_fetch))}/{len(ids_to_fetch)}")
if offset + args.batch_size < len(ids_to_fetch):
time.sleep(args.sleep)
papers = []
unresolved = []
for paper_id, local in selected.items():
remote = fetched.get(paper_id) or cached.get(paper_id)
if not remote:
unresolved.append({"arxiv_id": paper_id, **local})
continue
papers.append({**remote, **local})
papers.sort(key=lambda item: (item.get("published") or "", item["arxiv_id"]))
payload = {
"topic": args.topic,
"generated_at": date.today().isoformat(),
"selected_from_index": len(selected),
"resolved": len(papers),
"missing_arxiv_id": missing_ids,
"unresolved": unresolved,
"papers": papers,
}
write_json(output, payload)
print(json.dumps({key: payload[key] for key in ("selected_from_index", "resolved")}, indent=2))
return 0 if not unresolved else 2
if __name__ == "__main__":
raise SystemExit(main())
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env python3
"""Fast relevance screening for a broad Agent-memory research corpus."""
from __future__ import annotations
import argparse
import json
import re
import time
import urllib.error
import urllib.request
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PROMPT = """Screen papers for a research review about memory in LLM-based agents.
Use title and abstract only. Return JSON with a `papers` array in input order.
For each paper return:
- arxiv_id: exact input id
- relevance: core if memory is the main research object; supporting if memory is a substantial
mechanism, evaluation dimension, or failure mode; peripheral if memory is incidental, means
GPU/KV memory only, refers to human memory only, or the paper is mainly an unrelated application
- problem: choose one: access, representation, update_forgetting, execution_state, experience_learning,
evaluation, security_privacy, shared_memory, systems_cost, theory, application, unrelated
- reason: at most 12 words
Be conservative and output JSON only."""
RELEVANCE_VALUES = {"core", "supporting", "peripheral"}
PROBLEM_VALUES = {
"access",
"representation",
"update_forgetting",
"execution_state",
"experience_learning",
"evaluation",
"security_privacy",
"shared_memory",
"systems_cost",
"theory",
"application",
"unrelated",
}
PROBLEM_ALIASES = {
"adaptation": "experience_learning",
"continual_learning": "experience_learning",
"episodic": "representation",
"episodic_memory": "representation",
"experience": "experience_learning",
"forgetting": "update_forgetting",
"implicit_personalization": "application",
"memory": "representation",
"privacy": "security_privacy",
"security": "security_privacy",
}
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 generate(url: str, model: str, records: list[dict], timeout: int, retries: int) -> list[dict]:
compact = [
{"arxiv_id": item["arxiv_id"], "title": item["title"], "abstract": item["abstract"]}
for item in records
]
body = {
"model": model,
"prompt": f"{PROMPT}\n\nINPUT:\n{json.dumps(compact, ensure_ascii=False)}",
"stream": False,
"format": "json",
"options": {"temperature": 0, "num_predict": 4000},
}
request = urllib.request.Request(
f"{url.rstrip('/')}/api/generate",
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
for attempt in range(retries + 1):
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
result = json.loads(response.read().decode("utf-8"))
decoded = json.loads(result["response"])
if isinstance(decoded, list):
parsed = decoded
elif isinstance(decoded, dict) and isinstance(decoded.get("papers"), list):
parsed = decoded["papers"]
elif isinstance(decoded, dict) and isinstance(decoded.get("results"), list):
parsed = decoded["results"]
elif isinstance(decoded, dict) and decoded.get("arxiv_id"):
parsed = [decoded]
else:
raise ValueError("response lacks a recognized result array")
expected = [item["arxiv_id"] for item in records]
by_id = {str(item.get("arxiv_id")): item for item in parsed}
if any(paper_id not in by_id for paper_id in expected):
raise ValueError("response ids do not match input")
return [{**by_id[paper_id], "source": "model"} for paper_id in expected]
except (
urllib.error.HTTPError,
urllib.error.URLError,
TimeoutError,
json.JSONDecodeError,
KeyError,
ValueError,
):
if attempt >= retries:
raise
time.sleep(2 ** attempt)
raise RuntimeError("unreachable retry state")
def heuristic_screen(record: dict) -> dict:
title = record["title"].lower()
text = f"{record['title']} {record['abstract']}".lower()
title_memory = bool(re.search(r"memory|memori|remember|forget|stateful", title))
agent_memory = bool(
re.search(r"agent(?:ic)? memory|memory (?:for|in|of) .*agent|long-term .*agent", text)
)
hardware_only = bool(re.search(r"gpu memory|memory footprint|memory bandwidth|memory efficient", title))
if title_memory and not hardware_only:
relevance = "core"
elif agent_memory:
relevance = "supporting"
else:
relevance = "peripheral"
if re.search(r"attack|poison|privacy|security|trust|provenance|leak", text):
problem = "security_privacy"
elif re.search(r"benchmark|evaluation|diagnos|measure", title):
problem = "evaluation"
elif re.search(r"update|forget|consolidat|stale|supersed|write", text):
problem = "update_forgetting"
elif re.search(r"skill|experience|reflect|self-evolv|continual learn", text):
problem = "experience_learning"
elif re.search(r"execution state|task state|workflow state|long-horizon", text):
problem = "execution_state"
elif re.search(r"graph|hierarch|represent|structure|database|document", text):
problem = "representation"
elif re.search(r"retriev|search|recall|access", text):
problem = "access"
elif re.search(r"multi-agent|shared memory|collective memory", text):
problem = "shared_memory"
elif re.search(r"cost|latency|token|efficient|compress", text):
problem = "systems_cost"
else:
problem = "application" if relevance != "peripheral" else "unrelated"
return {
"arxiv_id": record["arxiv_id"],
"relevance": relevance,
"problem": problem,
"reason": "deterministic fallback after model-format failure",
"source": "heuristic",
}
def normalize_analysis(record: dict, analysis: dict | None) -> dict:
"""Constrain model output to the documented screening schema."""
fallback = heuristic_screen(record)
analysis = analysis if isinstance(analysis, dict) else {}
relevance = str(analysis.get("relevance", "")).strip().lower()
if relevance not in RELEVANCE_VALUES:
relevance = fallback["relevance"]
problem = str(analysis.get("problem", "")).strip().lower()
problem = PROBLEM_ALIASES.get(problem, problem)
if problem not in PROBLEM_VALUES:
problem = fallback["problem"]
reason = str(analysis.get("reason", "")).strip() or fallback["reason"]
source = str(analysis.get("source", "")).strip().lower()
if source not in {"model", "heuristic"}:
source = "model" if analysis else "heuristic"
return {
"arxiv_id": record["arxiv_id"],
"relevance": relevance,
"problem": problem,
"reason": reason,
"source": source,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--input",
type=Path,
default=ROOT / "data" / "research" / "agent-memory-expanded.json",
)
parser.add_argument(
"--output",
type=Path,
default=ROOT / "data" / "research" / "agent-memory-screened.json",
)
parser.add_argument("--ollama-url", default="http://192.168.1.10:11434")
parser.add_argument("--model", default="ChatGPT-5.6:Luna")
parser.add_argument("--batch-size", type=int, default=24)
parser.add_argument("--timeout", type=int, default=180)
parser.add_argument("--retries", type=int, default=2)
parser.add_argument("--refresh", action="store_true")
args = parser.parse_args()
args.input = args.input.resolve()
args.output = args.output.resolve()
source = json.loads(args.input.read_text(encoding="utf-8"))
records = source["papers"]
completed: dict[str, dict] = {}
if args.output.exists() and not args.refresh:
previous = json.loads(args.output.read_text(encoding="utf-8"))
completed = {
item["arxiv_id"]: {
**item,
"screen": normalize_analysis(item, item.get("screen")),
}
for item in previous.get("papers", [])
}
pending = [item for item in records if item["arxiv_id"] not in completed]
for offset in range(0, len(pending), args.batch_size):
batch = pending[offset : offset + args.batch_size]
try:
screened = generate(
args.ollama_url, args.model, batch, timeout=args.timeout, retries=args.retries
)
except Exception:
screened = []
for record in batch:
try:
screened.extend(
generate(
args.ollama_url,
args.model,
[record],
timeout=args.timeout,
retries=args.retries,
)
)
except Exception:
screened.append(heuristic_screen(record))
for record, analysis in zip(batch, screened, strict=True):
completed[record["arxiv_id"]] = {
**record,
"screen": normalize_analysis(record, analysis),
}
ordered = [completed[item["arxiv_id"]] for item in records if item["arxiv_id"] in completed]
write_json(
args.output,
{
"generated_at": date.today().isoformat(),
"model": args.model,
"source": str(args.input.relative_to(ROOT)),
"total": len(records),
"completed": len(ordered),
"papers": ordered,
},
)
print(f"screened {len(ordered)}/{len(records)}", flush=True)
# Rewrites an already complete file after schema normalization as well.
ordered = [completed[item["arxiv_id"]] for item in records if item["arxiv_id"] in completed]
write_json(
args.output,
{
"generated_at": date.today().isoformat(),
"model": args.model,
"source": str(args.input.relative_to(ROOT)),
"total": len(records),
"completed": len(ordered),
"papers": ordered,
},
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+194
View File
@@ -0,0 +1,194 @@
#!/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())