Skip to content

Commit 1fef185

Browse files
committed
fix: stabilize benchmark query expansion
1 parent 875a06f commit 1fef185

18 files changed

Lines changed: 413 additions & 261 deletions

benchmarks/longmemeval/longmemeval_runner.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import asyncio
3333
import json
3434
import logging
35-
import os
3635
import time
3736
from dataclasses import dataclass
3837
from datetime import datetime
@@ -51,8 +50,14 @@
5150
logger = logging.getLogger(__name__)
5251

5352

53+
def _base_question_type(question_type: str) -> str:
54+
"""Strip LongMemEval abstention suffix while preserving the real task type."""
55+
return question_type.removesuffix("_abs")
56+
57+
5458
# ── Config ────────────────────────────────────────────────────────────────────
5559

60+
5661
@dataclass
5762
class BenchmarkConfig:
5863
"""Configuration for a LongMemEval benchmark run."""
@@ -68,7 +73,7 @@ class BenchmarkConfig:
6873
extraction_model: str = "gemini:gemini-2.5-flash-lite"
6974

7075
# Retrieval
71-
retrieval_depth: str = "l1" # l0 | l1 | l2
76+
retrieval_depth: str = "l1" # l0 | l1 | l2
7277
top_k: int = 10
7378
context_window: int = 3
7479

@@ -86,6 +91,7 @@ class BenchmarkConfig:
8691

8792
# ── Runner ────────────────────────────────────────────────────────────────────
8893

94+
8995
class LongMemEvalBenchmark:
9096
"""Main benchmark runner."""
9197

@@ -128,7 +134,7 @@ async def _init_vektori(self) -> None:
128134
extraction_model=self.config.extraction_model,
129135
default_top_k=self.config.top_k,
130136
context_window=self.config.context_window,
131-
async_extraction=False, # benchmark needs sync so we can interleave cache ops
137+
async_extraction=False, # benchmark needs sync so we can interleave cache ops
132138
)
133139
await self.vektori_client._ensure_initialized()
134140
self.storage = self.vektori_client.db
@@ -218,10 +224,11 @@ async def _run_questions(self) -> None:
218224
if done % 10 == 0 or done == total:
219225
logger.info(
220226
"Progress: %d/%d questions answered (cache: %d sessions)",
221-
done, total, await self._session_cache.count(),
227+
done,
228+
total,
229+
await self._session_cache.count(),
222230
)
223231

224-
225232
except Exception as e:
226233
logger.error("Question %s failed: %s", qid, e, exc_info=True)
227234
self._checkpoint.mark_failed(qid, str(e))
@@ -251,7 +258,9 @@ async def _ingest_question(self, instance: dict[str, Any], user_id: str) -> None
251258

252259
cached_facts = await self._session_cache.get(hsid)
253260
if cached_facts is not None:
254-
await self._replay_session(session, hsid, user_id, session_time, session_date, cached_facts)
261+
await self._replay_session(
262+
session, hsid, user_id, session_time, session_date, cached_facts
263+
)
255264
else:
256265
new_facts = await self._full_ingest_session(
257266
session, hsid, user_id, session_time, session_date
@@ -293,9 +302,7 @@ async def _replay_session(
293302
)
294303

295304
if inserted_facts:
296-
conversation = "\n".join(
297-
f"{msg['role'].upper()}: {msg['content']}" for msg in session
298-
)
305+
conversation = "\n".join(f"{msg['role'].upper()}: {msg['content']}" for msg in session)
299306
try:
300307
await extractor._extract_episodes(
301308
inserted_facts=inserted_facts,
@@ -346,16 +353,15 @@ async def _full_ingest_session(
346353

347354
# ── Retrieval + QA ────────────────────────────────────────────────────────
348355

349-
async def _answer_question(
350-
self, instance: dict[str, Any], user_id: str
351-
) -> dict[str, Any]:
356+
async def _answer_question(self, instance: dict[str, Any], user_id: str) -> dict[str, Any]:
352357
qid = instance["question_id"]
353358
question = instance["question"]
354359
question_type = instance["question_type"]
360+
base_question_type = _base_question_type(question_type)
355361
question_date = instance.get("question_date") or ""
356362

357363
retrieval_t0 = time.perf_counter()
358-
use_expansion = question_type in ("multi-session", "temporal-reasoning")
364+
use_expansion = base_question_type in ("multi-session", "temporal-reasoning")
359365
search_results = await self.vektori_client.search(
360366
query=question,
361367
user_id=user_id,
@@ -450,7 +456,7 @@ async def _generate_answer(
450456

451457
llm = create_llm(self.config.eval_model)
452458
prompt = self._build_qa_prompt(question, context, question_type, question_date)
453-
max_tokens = 800 if question_type == "temporal-reasoning" else 500
459+
max_tokens = 800 if _base_question_type(question_type) == "temporal-reasoning" else 500
454460
try:
455461
return (await llm.generate(prompt, max_tokens=max_tokens)).strip()
456462
except Exception as e:
@@ -461,6 +467,7 @@ def _build_qa_prompt(
461467
self, question: str, context: str, question_type: str, question_date: str = ""
462468
) -> str:
463469
date_line = f"TODAY'S DATE: {question_date}\n\n" if question_date else ""
470+
base_question_type = _base_question_type(question_type)
464471

465472
abs_hint = ""
466473
if question_type.endswith("_abs"):
@@ -469,7 +476,7 @@ def _build_qa_prompt(
469476
"recognise that the information was never mentioned"
470477
)
471478

472-
if question_type == "temporal-reasoning":
479+
if base_question_type == "temporal-reasoning":
473480
return (
474481
"You are an AI assistant answering questions based on provided context "
475482
"from chat history.\n\n"
@@ -481,7 +488,8 @@ def _build_qa_prompt(
481488
"- First, list the relevant dated events from the context\n"
482489
"- Then compute the answer (count days/weeks/months between dates)\n"
483490
"- If the context does not contain enough information to answer, say "
484-
"\"I don't have that information\"\n\n"
491+
'"I don\'t have that information"'
492+
f"{abs_hint}\n\n"
485493
"REASONING:\n"
486494
)
487495

@@ -496,7 +504,7 @@ def _build_qa_prompt(
496504
"- Be concise and direct — a short phrase or sentence is preferred over a long explanation\n"
497505
"- For questions about time elapsed, use TODAY'S DATE above as your reference point\n"
498506
"- If the context does not contain enough information to answer, say "
499-
"\"I don't have that information\" — do not guess or infer beyond what is stated"
507+
'"I don\'t have that information" — do not guess or infer beyond what is stated'
500508
f"{abs_hint}\n\n"
501509
"ANSWER:"
502510
)
@@ -515,7 +523,10 @@ async def evaluate(self) -> None:
515523
jsonl_path = self.output_dir / f"{run_name}_qa_results.jsonl"
516524
with open(jsonl_path, "w", encoding="utf-8") as f:
517525
for r in qa_results:
518-
f.write(json.dumps({"question_id": r["question_id"], "hypothesis": r["hypothesis"]}) + "\n")
526+
f.write(
527+
json.dumps({"question_id": r["question_id"], "hypothesis": r["hypothesis"]})
528+
+ "\n"
529+
)
519530
logger.info("QA pairs saved to %s", jsonl_path)
520531

521532
self._metrics = self._compute_metrics(qa_results)
@@ -634,22 +645,20 @@ def _print_summary(self) -> None:
634645
if lat:
635646
print("\nLatency (ms):")
636647
print(
637-
" Ingestion avg={0} p95={1}".format(
648+
" Ingestion avg={} p95={}".format(
638649
lat.get("ingestion_avg"), lat.get("ingestion_p95")
639650
)
640651
)
641652
print(
642-
" Retrieval avg={0} p95={1}".format(
653+
" Retrieval avg={} p95={}".format(
643654
lat.get("retrieval_avg"), lat.get("retrieval_p95")
644655
)
645656
)
646657
print(
647-
" QA generation avg={0} p95={1}".format(
648-
lat.get("qa_avg"), lat.get("qa_p95")
649-
)
658+
" QA generation avg={} p95={}".format(lat.get("qa_avg"), lat.get("qa_p95"))
650659
)
651660
print(
652-
" Total/question avg={0} p95={1}".format(
661+
" Total/question avg={} p95={}".format(
653662
lat.get("total_question_avg"), lat.get("total_question_p95")
654663
)
655664
)
@@ -667,6 +676,7 @@ def _print_summary(self) -> None:
667676

668677
# ── Helpers ───────────────────────────────────────────────────────────────────
669678

679+
670680
def _parse_date(date_str: str) -> datetime | None:
671681
"""Parse a LongMemEval date string like '2023/05/30 (Tue) 23:40'."""
672682
if not date_str:
@@ -683,6 +693,7 @@ def _parse_date(date_str: str) -> datetime | None:
683693

684694
# ── CLI entry point ───────────────────────────────────────────────────────────
685695

696+
686697
async def main() -> None:
687698
import argparse
688699

test_conflict.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

test_conflict2.py

Lines changed: 0 additions & 44 deletions
This file was deleted.

test_llm_dedup_check.py

Lines changed: 0 additions & 38 deletions
This file was deleted.

test_synthesis.py

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)