- CI warm: ccache dep, continue-on-error, 60min timeout, YAMS_DISABLE_FAISS - Faiss: YAMS_HAS_FAISS guard, Apple Clang disable, OpenMP detection - Metadata: retry constraint failures, deduplicate batch UPSERT entries - Docker: wget for Zig, gfortran, YAMS_DISABLE_FAISS=1 - CI config: free-disk-space v1.3.1, libomp-18-dev, experimental trigger - Weekly benchmarks: faiss disable, ccache, diagnostics capture - Simeon: bump to 7763a97, EntropyRouter, concept_count - CorpusStats: extraction readiness fields - Remove committed artifacts (.wrangler, .artifacts, dispatcher_external) - Fix .vscode/settings.json hardcoded path Signed-off-by: trvon <git@trevon.dev>
196 lines
6.4 KiB
Python
Executable file
196 lines
6.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Convert LongMemEval_S from HuggingFace to BEIR format for yams benchmarking.
|
|
|
|
Downloads the cleaned LongMemEval_S dataset and converts it to BEIR format
|
|
(corpus.jsonl, queries.jsonl, qrels/test.tsv) so the existing
|
|
retrieval_quality_bench can consume it directly.
|
|
|
|
Usage:
|
|
uv run --with datasets scripts/prepare_longmemeval_s.py
|
|
uv run --with datasets scripts/prepare_longmemeval_s.py --max-questions 50
|
|
|
|
Output: ~/.cache/yams/benchmarks/longmemeval_s/
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def serialize_session(turns: list, date: str) -> str:
|
|
"""Serialize a chat session into searchable text.
|
|
|
|
Each turn may be a dict or a JSON string encoding {role, content}.
|
|
"""
|
|
lines = [f"[{date}]"]
|
|
for turn in turns:
|
|
if isinstance(turn, str):
|
|
turn = json.loads(turn)
|
|
role = turn.get("role", "unknown")
|
|
content = turn.get("content", "")
|
|
label = "User" if role == "user" else "Assistant"
|
|
lines.append(f"{label}: {content}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
default=Path.home() / ".cache" / "yams" / "benchmarks" / "longmemeval_s",
|
|
help="Output directory for BEIR-format files",
|
|
)
|
|
parser.add_argument(
|
|
"--max-questions",
|
|
type=int,
|
|
default=None,
|
|
help="Limit number of questions (for quick iteration)",
|
|
)
|
|
parser.add_argument(
|
|
"--include-abstention",
|
|
action="store_true",
|
|
help="Include false-premise (abstention) questions in qrels with score=0",
|
|
)
|
|
parser.add_argument(
|
|
"--full-haystack",
|
|
action="store_true",
|
|
help="Include all haystack sessions in corpus (default: only qrel-referenced)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
from datasets import load_dataset
|
|
except ImportError:
|
|
print(
|
|
"Error: 'datasets' package required.\n"
|
|
" uv pip install datasets\n"
|
|
"Or run with:\n"
|
|
" uv run --with datasets scripts/prepare_longmemeval_s.py",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
print("Loading LongMemEval_S from HuggingFace...")
|
|
# Stream to avoid downloading all splits (the _m split is >2GB and
|
|
# overflows pyarrow's int32 block size).
|
|
ds = load_dataset(
|
|
"xiaowu0162/longmemeval-cleaned",
|
|
split="longmemeval_s_cleaned",
|
|
streaming=True,
|
|
)
|
|
# Materialize the streamed dataset into a list
|
|
ds = list(ds)
|
|
|
|
if args.max_questions and args.max_questions < len(ds):
|
|
ds = ds[: args.max_questions]
|
|
print(f"Subsampled to {len(ds)} questions")
|
|
|
|
# First pass: collect qrel-referenced session IDs so we can filter corpus
|
|
needed_sids = set() # session IDs referenced by answer_session_ids
|
|
if not args.full_haystack:
|
|
for item in ds:
|
|
for sid in item.get("answer_session_ids", []):
|
|
needed_sids.add(sid)
|
|
print(f"Qrel-referenced sessions: {len(needed_sids)} (use --full-haystack for all)")
|
|
|
|
# Build corpus: one document per unique session, deduplicated across questions
|
|
corpus = {} # session_id -> {_id, title, text}
|
|
queries = [] # [{_id, text, metadata}]
|
|
qrels = [] # [(query_id, session_id, score)]
|
|
abstention_count = 0
|
|
question_type_counts = {}
|
|
|
|
for item in ds:
|
|
qid = item["question_id"]
|
|
qtype = item.get("question_type", "unknown")
|
|
question_type_counts[qtype] = question_type_counts.get(qtype, 0) + 1
|
|
|
|
is_abstention = qid.endswith("_abs") or qtype == "false_premise"
|
|
|
|
# Collect sessions into corpus
|
|
session_ids = item["haystack_session_ids"]
|
|
dates = item["haystack_dates"]
|
|
sessions = item["haystack_sessions"]
|
|
|
|
for sid, date, turns in zip(session_ids, dates, sessions):
|
|
if sid not in corpus and (args.full_haystack or sid in needed_sids):
|
|
corpus[sid] = {
|
|
"_id": sid,
|
|
"title": f"Chat Session - {date}",
|
|
"text": serialize_session(turns, date),
|
|
}
|
|
|
|
# Build query
|
|
queries.append(
|
|
{
|
|
"_id": qid,
|
|
"text": item["question"],
|
|
"metadata": {"question_type": qtype},
|
|
}
|
|
)
|
|
|
|
# Build qrels
|
|
answer_sids = item.get("answer_session_ids", [])
|
|
if is_abstention:
|
|
abstention_count += 1
|
|
if args.include_abstention:
|
|
for sid in answer_sids:
|
|
qrels.append((qid, sid, 0))
|
|
else:
|
|
for sid in answer_sids:
|
|
qrels.append((qid, sid, 1))
|
|
|
|
# Write output
|
|
out = args.output_dir
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
(out / "qrels").mkdir(exist_ok=True)
|
|
|
|
# corpus.jsonl
|
|
with open(out / "corpus.jsonl", "w") as f:
|
|
for doc in corpus.values():
|
|
f.write(json.dumps(doc, ensure_ascii=False) + "\n")
|
|
|
|
# queries.jsonl
|
|
with open(out / "queries.jsonl", "w") as f:
|
|
for q in queries:
|
|
f.write(json.dumps(q, ensure_ascii=False) + "\n")
|
|
|
|
# qrels/test.tsv
|
|
with open(out / "qrels" / "test.tsv", "w") as f:
|
|
f.write("query-id\tcorpus-id\tscore\n")
|
|
for qid, did, score in qrels:
|
|
f.write(f"{qid}\t{did}\t{score}\n")
|
|
|
|
# metadata.json
|
|
meta = {
|
|
"dataset": "LongMemEval_S",
|
|
"source": "xiaowu0162/longmemeval-cleaned",
|
|
"split": "longmemeval_s_cleaned",
|
|
"total_questions": len(queries),
|
|
"total_sessions": len(corpus),
|
|
"total_qrels": len(qrels),
|
|
"abstention_questions": abstention_count,
|
|
"include_abstention": args.include_abstention,
|
|
"question_type_counts": question_type_counts,
|
|
}
|
|
with open(out / "metadata.json", "w") as f:
|
|
json.dump(meta, f, indent=2)
|
|
|
|
print(f"\nDone! Output: {out}")
|
|
print(f" Corpus: {len(corpus)} sessions")
|
|
print(f" Queries: {len(queries)} questions")
|
|
print(f" Qrels: {len(qrels)} judgments")
|
|
print(f" Abstention (excluded): {abstention_count}")
|
|
print(f" Question types: {question_type_counts}")
|
|
print(
|
|
f"\nRun benchmark:\n"
|
|
f" YAMS_TEST_SAFE_SINGLE_INSTANCE=1 YAMS_BENCH_DATASET=longmemeval_s "
|
|
f"./builddir/tests/benchmarks/retrieval_quality_bench"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|