ReMe/benchmark/longmemeval/stats_agentic_answer.py
jinliyl bf7ca17705
feat(benchmark): add LongMemEval golden answer validation (#335)
* feat(benchmark): add golden answer validation and session review for LongMemEval

- Introduce GoldenCheckStep to validate LongMemEval golden answers using structured verdicts
- Add SessionReviewStep to extract query/answer-relevant evidence from all sessions
- Implement concurrent session processing with configurable concurrency limits
- Create check_golden job configuration with lme_review and lme_judge agent wrappers
- Add Qwen3.7-plus model configuration for enhanced processing capabilities
- Include python_execute tool integration for agent-based reasoning and date validation
- Generate comprehensive JSON output with session summaries and validation verdicts
- Add run_check_golden.py script for batch processing across all LongMemEval samples
- Configure proper logging initialization with console and file output options
- Update component registry and file I/O modules to support new benchmark features

* feat(scripts): add script to summarize LongMemEval check_golden verdicts

- Parse check_golden.json files across all LongMemEval samples
- Calculate accuracy metrics for golden answers and session IDs
- Provide breakdown by question type with percentage calculations
- Add command line options for listing bad samples and JSON output
- Include progress tracking showing completed vs pending samples
- Display confidence scores and date sanity checks statistics

* refactor(benchmark): move golden check scripts to longmemeval directory

- Moved run_check_golden.py from scripts/ to benchmark/longmemeval/
- Moved stats_check_golden.py from scripts/ to benchmark/longmemeval/
- Updated path resolution to use parents[2] instead of parent.parent
- Added new --list-run-failed option to stats script
- Added logging directory constant and functions for tracking launched samples
- Enhanced stats output with launched count and run failure information
- Improved error reporting with run failure details and log file paths

* feat(benchmark): add LongMemEval agentic answer workflow with session extraction

- Add LmeAgenticAnswerStep, LmeAutoMemoryStep, and LmeExtractSessionStep to __init__.py
- Create shared helper render_with_source for displaying search results with session_id
- Implement agentic_answer step with vector_search, bm25_search, and extract_session_by_id tools
- Add auto_memory step to convert each session into search-friendly daily notes
- Create extract_session step to retrieve and analyze raw session content by session_id
- Update jinli_lme.yaml with auto_memory, vector_search, bm25_search, and agentic_answer jobs
- Configure lme_memory, lme_extract, and lme_agentic_answer agent wrappers
- Enhance search steps with include_source option to show session_id metadata
- Add proper session_id tracking and collision handling in daily note generation

* feat(benchmark): add LongMemEval agentic answer evaluation pipeline

- Add session_id tracking to agentic_answer.py result metadata
- Introduce run_agentic_answer.py driver for complete pipeline execution
- Implement auto_memory, update_index, and agentic_answer job orchestration
- Add concurrent execution with configurable limits and staggering
- Create aggregation script for collecting tool-call trails and results
- Add stats_agentic_answer.py for comprehensive result analysis
- Implement resume capability with existing output detection
- Generate aggregate.json with per-sample breakdown and tool call summaries

* feat(steps): add ClearPathsStep for cleaning workspace outputs before rebuild

- Introduce ClearPathsStep to remove stale workspace files/directories
- Add support for specifying paths and config_keys as targets to clear
- Implement safety checks to prevent deletion of files outside workspace
- Add logging for cleared paths and warnings for invalid paths
- Configure clear_paths_step in jinli_lme.yaml to clean daily_dir
- Add clear_paths_step to clean mem_answer.json before rebuilds

* feat(benchmark): add resume functionality to agentic answer runner

- Replace --force flag with --resume flag for controlling job execution
- By default every job reruns with clean rebuild behavior using config clear steps
- Add --resume option to skip samples whose output already exists and continue interrupted batches
- Update documentation to reflect new default clean rebuild behavior
- Modify job skipping logic to honor resume flag instead of force flag
- Update dry-run output to show correct todo jobs based on resume status
- Change default example command to use --resume for continuing interrupted runs

* feat(benchmark): generate JSONL output for check golden records

- Add write_check_golden_list function to create JSONL file
- Write all readable check_golden records as JSONL format
- Include check_golden_list path in stats output
- Display generated JSONL file path in summary report
- Maintain UTF-8 encoding with non-ASCII character support

* refactor(benchmark): rename answer judge step and integrate LME LLM judge

- Rename AnswerJudgeStep to LmeLlmJudgeStep and update imports
- Add new llm_judge configuration in jinli_lme.yaml
- Update run_agentic_answer.py to include llm_judge in pipeline
- Modify LmeLlmJudgeStep to read from query.json and answer.json
- Write LLM judgement results back to mem_answer.json
- Add command line options for start/end sample range selection
- Update aggregate.json generation to include LLM judgement data
- Add resume capability for llm_judge job based on judgement presence

* refactor(benchmark): rename answer judge step and integrate LME LLM judge

- Rename AnswerJudgeStep to LmeLlmJudgeStep and update imports
- Add new llm_judge configuration in jinli_lme.yaml
- Update run_agentic_answer.py to include llm_judge in pipeline
- Modify LmeLlmJudgeStep to read from query.json and answer.json
- Write LLM judgement results back to mem_answer.json
- Add command line options for start/end sample range selection
- Update aggregate.json generation to include LLM judgement data
- Add resume capability for llm_judge job based on judgement presence

* feat(steps): add wait_for_paths_step to block until workspace files exist

- Introduce WaitForPathsStep class that polls for required workspace-relative paths
- Add step registration with 'wait_for_paths_step' backend identifier
- Implement path validation to ensure targets are within workspace boundaries
- Add polling mechanism with configurable intervals via poll_seconds parameter
- Include logging functionality with log_every_seconds parameter for status updates
- Add metadata tracking of waited paths and duration in response object
- Register step in index module and expose in public API
- Configure step in jinli_lme.yaml to wait for session_review.json before golden check
- Add script rename from run_check_golden.py to run_golden_check.py with enhanced options

* feat(benchmark): enhance longmemeval benchmarking with concurrency and progress tracking

- Add benchmark extra dependency group with portalocker requirement
- Introduce concurrent execution support for golden_check and session_review workflows
- Add progress reporting interval option with real-time status updates
- Implement global throttling mechanism for session review requests using file locks
- Enhance golden check validation with current schema verification
- Add active task tracking and graceful shutdown handling
- Rename check_golden scripts to golden_check for consistency
- Update statistics reporting with correct/incorrect terminology instead of reasonable
- Add stale format detection and compatibility handling for verdict fields
- Include both_correct rate calculation in accuracy metrics
- Add concurrency and staggering options for better resource management

* ci(workflow): add Windows smoke test workflow

- Create new workflow file .github/workflows/windows-smoke.yml
- Configure workflow to trigger on push and pull request events
- Set up Python environment with version 3.11
- Install package dependencies using pip
- Run version job as smoke test for CLI functionality
- Enable concurrency control to prevent duplicate runs
- Use matrix strategy for Python version testing

* feat(benchmark): add retry mechanism and health check for session review

- Added retry configuration options (retry_initial_seconds, retry_max_seconds, retry_max_attempts) to jinli_lme.yaml
- Implemented exponential backoff retry logic with configurable parameters in session_review step
- Added output_is_healthy function to verify session_review.json integrity and absence of failed reviews
- Updated resume functionality to skip only healthy outputs instead of all existing files
- Integrated JSON parsing and validation to check for failed reviews in output files
- Enhanced error handling and logging for retry attempts and recovery scenarios

* feat(benchmark): add LongMemEval session review statistics script

- Create stats_session_review.py to summarize session_review.json artifacts
- Add command line options for listing failed, missing, and run failed samples
- Implement JSON output mode for programmatic consumption
- Calculate and display health statistics including total samples, healthy outputs, failed sessions
- Provide detailed failure information with session IDs and error messages
- Generate re-run commands for samples with failed reviews
- Add percentage calculations for better statistical overview
- Include support for multiple output formats and detailed logging

* feat(benchmark): add LongMemEval output cleanup script and enhance golden check retry logic

- Added clean_sample_outputs.py script to remove generated LongMemEval files while preserving source inputs
- Implemented configurable retry mechanism in golden_check.py with exponential backoff strategy
- Added retry parameters (initial/max seconds and max attempts) to control failure recovery behavior
- Integrated asyncio support for asynchronous sleep during retry intervals
- Configured default retry settings in jinli_lme.yaml with 5s initial and 300s maximum intervals
- Preserved core files (query.json, answer.json, session/) while cleaning generated artifacts

* feat(benchmark): add AppleDouble file cleanup to sample output cleaner

- Remove AppleDouble files starting with '._' recursively including under session/
- Add is_under helper function to check if path is inside parent directory
- Track targets in set to avoid duplicate processing
- Include AppleDouble files in cleanup targets when not already covered by existing targets
- Maintain dry-run mode as default behavior with --apply flag for actual deletion

* refactor(benchmark): update LongMemEval sample output cleaning script

- Add time and Iterator imports for enhanced functionality
- Add --progress-every argument to control progress reporting frequency
- Replace is_under function with iter_sample_targets generator
- Implement detailed progress tracking with timing measurements
- Add sample-by-sample processing with elapsed time reporting
- Include AppleDouble file detection within session directory
- Update target counting and deletion statistics display
- Add conditional progress updates based on progress-every setting
- Improve dry-run mode with would-delete indication

* chore(benchmark): increase initial interval for session review step

- Changed START_INTERVAL_SECONDS from 1.0 to 3.0 seconds
- Adjusted timing parameters for better benchmark stability

* refactor(benchmark): implement coordinated retry mechanism for session reviews

- Add retry gate condition to coordinate concurrent review attempts
- Implement wait_for_healthy_start_slot to handle sequential retries
- Create mark_retrying and mark_recovered functions to track retry states
- Update reply_with_retry to accept index parameter for coordination
- Add has_prior_retry logic to prevent race conditions during recovery
- Ensure proper cleanup of retry state on success or failure
- Maintain backward compatibility while adding coordination features

* chore(benchmark): adjust session review start interval timeout

- Changed START_INTERVAL_SECONDS from 3.0 to 5.0 seconds
- Increased initial delay for session review benchmark step
- Updated timeout configuration for improved stability

* refactor(benchmark): update session review concurrency and throttling mechanism

- Replace global throttle with per-process concurrency control
- Add concurrency parameter with default value of 30 in config
- Add start_interval_seconds parameter with default value of 2 seconds
- Change default concurrency from 3 to 1 in command line interface
- Update documentation to reflect new throttling behavior
- Implement semaphore-based concurrency limiting for review tasks
- Modify retry mechanism to use local locking instead of global files
- Remove portalocker dependency for cross-process throttling

* refactor(config): update session review configuration and concurrency settings

- Removed deprecated retry configuration parameters from jinli_lme.yaml
- Increased MAX_CONCURRENCY from 30 to 60 in session_review.py
- Reduced START_INTERVAL_SECONDS from 2.0 to 1.0 in session_review.py
- Cleaned up redundant backend specifications in configuration file
- Simplified agent wrapper configurations by removing obsolete retry settings

* feat(benchmark): enhance LME auto memory step with advanced scheduling and error handling

- Add datetime parsing functionality for LongMemEval timestamps with regex pattern
- Implement configurable concurrency limits with MAX_CONCURRENCY of 60
- Introduce retry mechanism with exponential backoff for agent interactions
- Add session filtering based on date comparison with question_date validation
- Create rate limiting with start interval control between requests
- Implement sophisticated retry coordination using asyncio conditions
- Add comprehensive error tracking for failed and filtered session extracts
- Remove deprecated concurrency parameter from jinli_lme.yaml configuration
- Add structured output validation in session review step
- Include detailed metadata reporting with session statistics and errors

* fix(benchmark): adjust default concurrency for auto_memory job

- Changed default concurrency from 3 to 1 for auto_memory job to prevent API overload
- Updated help text to reflect new default value of 1 for concurrency parameter
- Modified documentation to clarify concurrency behavior varies by job type

* refactor(search): replace hardcoded candidate multiplier with constant

- Introduced _CANDIDATE_MULTIPLIER constant set to 10
- Replaced hardcoded factor of 5 with _CANDIDATE_MULTIPLIER in BM25 search
- Replaced hardcoded factor of 5 with _CANDIDATE_MULTIPLIER in vector search
- Updated test to verify both search steps use ten times limit for candidates
- Imported VectorSearchStep and Bm25SearchStep in test module
- Added comprehensive test case for candidate count calculation logic

* feat(lme): add data inspection error handling with fallback mechanism

- Implemented non-retryable data inspection error markers detection
- Added _is_data_inspection_error method to identify inspection failures
- Created fallback handling for data inspection errors in auto memory extraction
- Added fallback handling for data inspection errors in session review
- Extended failed extracts tracking with non-retryable and fallback flags
- Separated fallback extracts from regular failed extracts in reporting
- Enhanced error logging with specific data inspection failure messages
- Updated metrics to track fallback extractions and reviews separately
- Maintained existing retry logic for other exception types

* feat(benchmark): enhance session review statistics with fallback tracking

- Add support for identifying and listing non-retryable fallback reviews
- Introduce --list-fallback argument to display fallback review details
- Separate retryable failures from non-retryable fallbacks in reporting
- Track fallback samples and sessions separately from failed ones
- Update console output to show both retryable and non-retryable categories
- Include fallback details in JSON output with reasons and session info
- Modify failure counting logic to distinguish between retryable and fallback reviews

* feat(benchmark): add question_id tracking and enhanced fallback reporting

- Add question_id function to extract query.question_id from data
- Initialize question_id_by_id dictionary to store question IDs by index
- Store question_id for each sample during data processing
- Enhance fallback output to include question IDs and session information
- Format sample labels with question IDs when available
- Display session IDs associated with each fallback case

* feat(benchmark): add question_id support and improve bad sample reporting

- Add question_id_for function to extract question_id from multiple sources
- Add sample_label function to format samples as idx(question_id) when available
- Store question_id in data dictionary during processing
- Change bad_golden and bad_sessions to store full records instead of just indices
- Update list_bad output to show formatted labels with question_id information
- Improve error reporting with more detailed sample identification

* feat(benchmark): enhance golden check stats with structured output

- Add related_session_ids function to extract session IDs from verdict records
- Create grouped_records function to group records by question type
- Replace flat list output with JSON-formatted grouped records in list_bad option
- Replace flat list output with JSON-formatted grouped records in list_bad_sessions option
- Maintain Chinese labels while adding structured data presentation
- Improve readability of bad verdict record display with hierarchical grouping

* feat(benchmark): update data structure for question indexing

- Replace sample_label with _idx field for index tracking
- Add question_id field to store _question_id values
- Maintain backward compatibility with empty string defaults
- Preserve existing session_id functionality
- Update data mapping to include new fields in grouped results

* refactor(benchmark): streamline golden answer verification process

- Replace relevance filtering with comprehensive information extraction
- Remove is_relevant field and simplify session summary structure
- Change relevant_info to extracted_info for clarity
- Update golden check logic to work with full extractions instead of filtered summaries
- Simplify prompt instructions to focus on complete information extraction
- Remove redundant schema validation and structured output requirements
- Adjust statistics calculation to match new extraction approach
- Update metadata field names to reflect extraction rather than relevance checking

* feat(benchmark): add selective file deletion option to clean_sample_outputs

- Add --filename argument to delete only specific root-level files
- Modify iter_sample_targets function to accept optional filenames filter
- Implement validation for root-level filename constraints
- Update function calls to pass filenames parameter
- Add example usage for selective file deletion in documentation

* feat(benchmark): add error count metrics to golden check statistics

- Added golden_bad, session_bad, and both_bad calculation fields
- Updated console output format to include error counts per question type
- Modified table display to show both accuracy rates and error numbers
- Enhanced statistical summary with additional error breakdown metrics

* test(search): update search step tests with include_source parameter

- Added include_source=False parameter to VectorSearchStep initialization
- Added include_source=False parameter to Bm25SearchStep initialization
- Maintained existing RuntimeContext parameters for both search steps
- Updated test calls to match new constructor signature with include_source option
2026-07-13 21:26:27 +08:00

202 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Summarise the ``agentic_answer`` results across all LongMemEval samples.
Reports progress (how many of the 500 samples produced ``mem_answer.json``) and a
breakdown of answer *status*:
- answered — a non-empty answer that is not "not provided";
- not_provided — the agent gave up ("not provided");
- empty — ``mem_answer.json`` exists but the answer is blank;
- missing — no ``mem_answer.json`` yet.
Everything is broken down by ``question_type``. This script does NOT judge answer
correctness (there is no grader for ``mem_answer`` yet) — it only tracks progress
and collects predicted-vs-golden pairs. Tool-call statistics are read from the
aggregate written by ``run_agentic_answer.py`` when it is present.
Examples:
python benchmark/longmemeval/stats_agentic_answer.py
python benchmark/longmemeval/stats_agentic_answer.py --list-run-failed
python benchmark/longmemeval/stats_agentic_answer.py --list-unanswered
python benchmark/longmemeval/stats_agentic_answer.py --json
"""
import argparse
import json
from collections import defaultdict
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
LOGBASE = REPO / "logs" / "agentic_answer"
AGGREGATE = LOGBASE / "aggregate.json"
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--list-unanswered", action="store_true", help="list samples answered 'not provided' or empty")
p.add_argument("--list-run-failed", action="store_true", help="list launched samples with no readable output")
p.add_argument("--json", action="store_true", help="emit the summary as JSON")
return p.parse_args()
def sample_ids() -> list[str]:
"""List all sample IDs (numeric workspace dirs), numerically sorted."""
ids = [p.name for p in DATA.iterdir() if p.is_dir() and p.name.isdigit()]
return sorted(ids, key=int)
def pct(num: int, den: int) -> str:
"""Format a percentage."""
return f"{(100.0 * num / den):.1f}%" if den else "n/a"
def logged_sample_ids() -> list[str]:
"""List sample IDs that have an agentic_answer launch log."""
logdir = LOGBASE / "agentic_answer"
if not logdir.exists():
return []
ids = [p.stem for p in logdir.glob("*.log") if p.stem.isdigit()]
return sorted(ids, key=int)
def answer_status(pred: str, has_file: bool) -> str:
"""Classify an answer into answered / not_provided / empty / missing."""
if not has_file:
return "missing"
if not pred:
return "empty"
if "not provided" in pred.lower():
return "not_provided"
return "answered"
def load_tool_calls() -> dict[str, int]:
"""Map idx -> num_tool_calls from the aggregate, if it exists."""
if not AGGREGATE.exists():
return {}
try:
with AGGREGATE.open(encoding="utf-8") as f:
data = json.load(f)
except (OSError, json.JSONDecodeError):
return {}
return {s["idx"]: s.get("num_tool_calls", 0) for s in data.get("samples", []) if "idx" in s}
def main() -> int:
"""Main entry point."""
args = parse_args()
ids = sample_ids()
total = len(ids)
tool_calls = load_tool_calls()
rows, unreadable = [], []
finished_ids = set()
for idx in ids:
query_path = DATA / idx / "query.json"
mem_path = DATA / idx / "mem_answer.json"
qtype = "(unknown)"
try:
with query_path.open(encoding="utf-8") as f:
qtype = json.load(f).get("question_type") or "(unknown)"
except (OSError, json.JSONDecodeError):
pass
has_file = mem_path.exists()
pred = ""
if has_file:
try:
with mem_path.open(encoding="utf-8") as f:
pred = str(json.load(f).get("answer") or "").strip()
finished_ids.add(idx)
except (OSError, json.JSONDecodeError):
unreadable.append(idx)
has_file = False
rows.append({"idx": idx, "type": qtype, "status": answer_status(pred, has_file)})
finished = [r for r in rows if r["status"] != "missing"]
n = len(finished)
launched = logged_sample_ids()
run_failed = [idx for idx in launched if idx not in finished_ids]
# Overall status tallies.
status_counts: dict[str, int] = defaultdict(int)
for r in rows:
status_counts[r["status"]] += 1
answered = status_counts["answered"]
unanswered = [r["idx"] for r in rows if r["status"] in ("not_provided", "empty")]
calls_vals = [tool_calls[i] for i in finished_ids if i in tool_calls]
avg_calls = sum(calls_vals) / len(calls_vals) if calls_vals else 0.0
# Per question_type breakdown.
by_type: dict[str, dict[str, int]] = defaultdict(lambda: {"n": 0, "answered": 0})
for r in finished:
by_type[r["type"]]["n"] += 1
by_type[r["type"]]["answered"] += 1 if r["status"] == "answered" else 0
if args.json:
print(
json.dumps(
{
"total": total,
"finished": n,
"pending": total - n - len(unreadable),
"unreadable": unreadable,
"launched": len(launched),
"run_failed": run_failed,
"status_counts": dict(status_counts),
"answered_rate": round(answered / n, 4) if n else None,
"avg_tool_calls": round(avg_calls, 2) if calls_vals else None,
"by_type": {
t: {**c, "answered_rate": round(c["answered"] / c["n"], 4)} for t, c in by_type.items()
},
"unanswered": unanswered,
"aggregate": str(AGGREGATE) if AGGREGATE.exists() else None,
},
ensure_ascii=False,
indent=2,
),
)
return 0
print("=" * 60)
print("LongMemEval agentic_answer 统计")
print("=" * 60)
print(f"样例总数 : {total}")
print(f"已完成 (有产出) : {n} ({pct(n, total)})")
print(f"未完成 : {total - n - len(unreadable)}")
if unreadable:
print(f"损坏/无法解析 : {len(unreadable)} {unreadable}")
print(f"已启动过 (有 log) : {len(launched)}")
print(f"运行失败/无可读产出 : {len(run_failed)}")
print("-" * 60)
print(f"已作答 (非 not provided): {answered} ({pct(answered, n)} of finished)")
print(f" 其中 not provided : {status_counts['not_provided']}")
print(f" 其中 空答案 : {status_counts['empty']}")
if calls_vals:
print(f"平均工具调用次数 : {avg_calls:.1f} (来自 {AGGREGATE.name})")
else:
print("平均工具调用次数 : n/a (先跑 run_agentic_answer.py 生成 aggregate.json)")
print("-" * 60)
print("按 question_type:")
print(f" {'type':<24} {'n':>4} {'已作答率':>12}")
for t in sorted(by_type):
c = by_type[t]
print(f" {t:<24} {c['n']:>4} {pct(c['answered'], c['n']):>12}")
if args.list_unanswered:
print("-" * 60)
print(f"not provided / 空答案的样例 ({len(unanswered)}): {unanswered}")
if args.list_run_failed:
print("-" * 60)
print(f"运行失败/无可读 mem_answer.json 的样例 ({len(run_failed)}): {run_failed}")
for idx in run_failed:
print(f" {idx}: {LOGBASE / 'agentic_answer' / f'{idx}.log'}")
print("=" * 60)
return 0
if __name__ == "__main__":
raise SystemExit(main())