#!/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())