3232import asyncio
3333import json
3434import logging
35- import os
3635import time
3736from dataclasses import dataclass
3837from datetime import datetime
5150logger = 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
5762class 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+
8995class 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 ("\n Latency (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+
670680def _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+
686697async def main () -> None :
687698 import argparse
688699
0 commit comments