Skip to content

feat: add lightweight GraphRAG benchmark - #77

Closed
Postroggy wants to merge 18 commits into
hugegraph:mainfrom
Postroggy:feat/graphrag-benchmark-issue7
Closed

feat: add lightweight GraphRAG benchmark#77
Postroggy wants to merge 18 commits into
hugegraph:mainfrom
Postroggy:feat/graphrag-benchmark-issue7

Conversation

@Postroggy

@Postroggy Postroggy commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Fixes #75.

This PR adds a lightweight, reproducible GraphRAG benchmark module for hugegraph-llm. It is designed for fast local evaluation after changing graph extraction, retrieval, rerank, context construction, or answer generation logic.

The benchmark intentionally keeps the base evaluation path offline and deterministic. LLM-as-Judge is available as an optional enhancement, but basic extraction, retrieval, answer, baseline, and report workflows do not require external LLM services.

Background

Issue #75 asks for a lightweight benchmark capability because HugeGraph-LLM already has GraphRAG features, but did not have a convenient way to answer these questions after changes:

  • Did graph extraction become more complete or more correct?
  • Did retrieval recall the expected evidence?
  • Did a candidate run improve or regress against a saved baseline?
  • Can the report be reproduced locally and pasted into a PR or issue comment?
  • Can Chinese and English examples be evaluated without relying on heavyweight external frameworks?

This PR adds that missing benchmark layer as an independent package under hugegraph-llm, without changing the existing GraphRAG runtime pipeline.

What Changed

Benchmark CLI

Adds hugegraph_llm.benchmark with a runnable CLI:

uv run python -m hugegraph_llm.benchmark run \
  --mode retrieval \
  --data src/hugegraph_llm/benchmark/data/samples/retrieval_sample.json \
  --metrics recall_at_k,hit_at_k,mrr \
  --offline \
  --format markdown

Supported commands include:

  • run: run extraction, retrieval, or ablation evaluation
  • baseline save: persist a benchmark result as a named baseline
  • compare: compare candidate results with a baseline and surface metric deltas plus sample-level changes
  • list-metrics: inspect registered metrics

Evaluation Runners

Adds three benchmark runners:

  • ExtractionRunner: evaluates graph extraction quality from expected graph and candidate graph outputs
  • RetrievalRunner: evaluates evidence retrieval quality from questions, expected evidence, and candidate retrieved contexts
  • AblationRunner: compares answer variants such as raw, vector-only, graph-only, and graph+vector answers

The runners share common behavior for:

  • sample-level error isolation
  • concurrent execution
  • deterministic sample ordering
  • metric registry lookup
  • JSON and Markdown report generation

Metrics

Adds offline-first metrics for the required base benchmark path.

Extraction metrics:

  • entity_f1
  • triple_f1
  • property_f1
  • schema_validity
  • structural_integrity
  • syntax_validity
  • graph_structure
  • conflict_detection
  • temporal_validity

Retrieval metrics:

  • recall_at_k
  • hit_at_k
  • mrr
  • context_precision
  • context_relevancy
  • evidence_recall_llm

Answer / ablation metrics:

  • token_f1
  • exact_match
  • rouge_l
  • answer_correctness
  • faithfulness
  • coverage

The LLM-Judge metrics are optional. Offline metrics are sufficient for the default benchmark workflow.

Chinese and English Support

Adds bilingual sample data and normalization utilities for Chinese and English evaluation:

  • Chinese punctuation normalization
  • full-width to half-width conversion
  • optional traditional-to-simplified conversion when opencc is available
  • Chinese tokenization via jieba
  • English normalization and optional stemming behavior
  • Chinese and English LLM-Judge prompts

This addresses the issue requirement that the benchmark should be friendly to Chinese scenarios instead of assuming English-only whitespace tokenization.

Baseline and Candidate Comparison

Adds persistent baseline storage and comparison support:

  • save one run result as a baseline
  • compare candidate results against a baseline
  • report metric deltas
  • include sample-level failures or regressions instead of only average scores

This is intended for PR workflows where contributors need to show whether a GraphRAG change improves or regresses benchmark behavior.

Reports

Adds both JSON and Markdown reporters:

  • JSON for automation and downstream tooling
  • Markdown for PR / issue comments

The Markdown report includes aggregate metrics and per-sample details so reviewers can inspect concrete failure or regression examples.

Dataset Preparation and Experiment Scripts

Adds dataset preparation and helper scripts under hugegraph-llm/scripts/benchmark/:

  • sample benchmark runs
  • external dataset conversion entry points
  • small dataset experiment runner
  • HotpotQA demo scripts

Generated experiment outputs and downloaded benchmark data are kept out of git by .gitignore.

Documentation

Adds detailed documentation in:

  • hugegraph-llm/GRAPHRAG_BENCHMARK.md
  • hugegraph-llm/scripts/benchmark/README.md
  • docs/quality/benchmark-code-style-spec.md

The main benchmark document covers:

  • motivation and scope
  • metric taxonomy
  • CLI usage
  • how to add new cases
  • how to save and compare baselines
  • how to interpret JSON and Markdown reports
  • robustness design
  • Chinese / English behavior
  • small real-dataset validation results

Requirement Mapping for Issue #75

Issue #75 requirement PR coverage
Research existing RAG / GraphRAG evaluation approaches and explain tradeoffs Documented in GRAPHRAG_BENCHMARK.md, including why the implementation borrows metric ideas without adding heavyweight framework dependencies
Run GraphRAG benchmark from CLI python -m hugegraph_llm.benchmark CLI with run / baseline / compare commands
Evaluate graph extraction quality Extraction runner plus entity, triple, property, schema, syntax, structure, conflict, and temporal metrics
Focus on completeness and correctness Entity / triple / property precision-recall-F1, schema validity, structural integrity, conflict detection, temporal validity
Evaluate retrieval quality Retrieval runner plus Recall@K, Hit@K, MRR, context precision, context relevancy, and evidence recall
Support baseline / candidate / reference comparison Baseline store plus compare command and per-sample delta reporting
Save one run as baseline baseline save command
Output JSON and Markdown reports JSON reporter and Markdown reporter
Include graph extraction sample English and Chinese-style graph extraction sample fixtures, including car-domain sample
Include retrieval sample Retrieval sample fixtures including Chinese retrieval sample
Cover Chinese and English examples Bilingual sample data, bilingual prompts, and language-aware normalization
Report failed / regressed samples, not only averages Markdown and JSON reports include sample-level details and comparison deltas
Document how to add cases, run benchmark, compare results, and interpret reports Covered in GRAPHRAG_BENCHMARK.md and scripts README
Basic evaluation should not require external LLM Offline metrics and mock judge path are available; LLM-Judge metrics are opt-in

Design Notes

  • The benchmark is isolated under hugegraph_llm.benchmark and does not modify existing GraphRAG runtime behavior.
  • The metric registry keeps metric lookup explicit and avoids hidden runtime imports.
  • Runners isolate sample-level and metric-level failures so one bad sample does not fail the whole benchmark run.
  • Optional LLM-Judge metrics use JSON self-healing parsing and retry logic for provider instability.
  • External-service usage is opt-in and visible through CLI options; default tests use deterministic local fixtures.

Validation

Local checks passed:

uv run ruff check .
uv run ruff format --check .
cd hugegraph-llm && uv run pytest src/tests/benchmark -q

Test result:

218 passed, 1 warning

The warning is from jieba importing deprecated pkg_resources through its dependency path. It does not affect benchmark behavior.

Real Dataset Smoke Results

Small real-dataset checks were run before opening this PR. These are intentionally small because the PR should stay lightweight and reproducible.

  • Text2KGBench Movie extraction oracle, 5 samples: extraction metric chain runs successfully on real ontology / triples format
  • GraphRAG-Bench Medical retrieval, 5 samples: offline retrieval metrics run successfully on real question / evidence / corpus format
  • GraphRAG-Bench Medical controlled ablation, 3 samples: answer metrics distinguish strong and weak controlled variants
  • Tiny LLM-Judge checks, 1 sample each: optional LLM-Judge retrieval and answer metrics run successfully through the CLI fallback path

The extraction oracle and controlled ablation results are sanity checks for metric coverage and distinguishability. They are not presented as production GraphRAG model quality numbers.

Scope and Non-Goals

This PR does not change the existing GraphRAG pipeline implementation. It adds the benchmark layer needed to evaluate future GraphRAG changes.

This PR also does not make external LLM providers mandatory for benchmark usage. LLM-Judge remains optional because Issue #75 explicitly asks that the basic benchmark should not depend on external LLM services.

Summary by CodeRabbit

  • 新功能
    • 新增 HugeGraph-LLM 基准评测 CLI:支持提取/检索/消融模式运行、离线跳过、样本裁剪、结果导出与 baseline 保存、对比回归分析(含 JSON/Markdown 报告)。
    • 扩展评测能力:新增/完善答案、检索、抽取与语义匹配/忠实度等指标(含 LLM-Judge 与 Mock)。
    • 新增公开数据集下载与转换工具,统一基准输入格式,并提供示例样本文件。
  • 文档
    • 补充基准数据集使用指南、Issue #75 评测方案与完整实验记录/复现步骤。
  • 维护
    • 忽略本地生成的外部基准数据目录与相关文件。

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

@codecov-ai-reviewer review

@github-actions github-actions Bot added the llm label Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

本 PR 新增 hugegraph-llm 的独立 benchmark 子系统,覆盖答案、抽取、检索三类指标,补齐基线保存/对比、Markdown/JSON 报告、CLI 运行入口、外部数据集转换、pipeline 适配和大量测试与文档。

Changes

GraphRAG Benchmark 评测能力

Layer / File(s) Summary
文档、配置与样例
.gitignore, hugegraph-llm/BENCHMARK_DATASETS.md, hugegraph-llm/GRAPHRAG_BENCHMARK.md, hugegraph-llm/docs/benchmark/*, hugegraph-llm/pyproject.toml, hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/*
新增 benchmark 使用/设计文档、实验记录与报告,忽略本地基准数据目录,增加 rouge_score 依赖与 hugegraph-benchmark 命令入口,并补充抽取、检索、消融样例。
核心模型、注册表与基础工具
benchmark/metrics/base.py, benchmark/metrics/registry.py, benchmark/models/*, benchmark/utils/normalize.py, benchmark/metrics/dimensions.py, benchmark/llm_judge/*, tests/benchmark/test_registry_fix.py, test_json_parse_utils.py, test_llm_judge_metrics.py
定义指标抽象、结果模型、注册表、归一化与维度映射,并实现 LLM-Judge 抽象、提示词、JSON 修复解析、Mock/Real judge 与对应测试。
答案类指标
benchmark/metrics/answer/*, tests/benchmark/test_answer_metrics.py
实现 TokenF1、ExactMatch、RougeL、Faithfulness、AnswerCorrectness、Coverage 六项答案指标及其测试。
抽取类指标
benchmark/metrics/extraction/*, tests/benchmark/test_extraction_metrics.py, test_conflict_detection.py, test_graph_structure.py, test_temporal_validity.py
实现实体/三元组/属性 F1、Schema 合法性、结构完整性、语法合法性、图结构、冲突检测、时态有效性、语义匹配与忠实性指标。
检索类指标
benchmark/metrics/retrieval/*, tests/benchmark/test_retrieval_metrics.py
实现 RecallAtK、HitAtK、MRR、ContextPrecision、ContextRelevancy、EvidenceRecallLLM 六项检索指标。
Pipeline 适配工具
benchmark/utils/graph_extract.py, benchmark/utils/retrieval_adapter.py, tests/benchmark/test_graph_extract.py, test_retrieval_adapter.py
将真实 HugeGraph-LLM pipeline 输出归一化为 benchmark 输入格式。
Runner、基线与报告
benchmark/runners/*, benchmark/baseline/*, benchmark/reporters/*, tests/benchmark/test_base_runner.py, test_baseline.py, test_markdown_reporter.py, test_reproducibility.py, test_integration_*.py, test_e2e_*.py
实现并发 Runner、基线存储/对比与 JSON/Markdown 报告,并提供单元、集成和端到端测试。
CLI 与外部数据集转换
benchmark/cli.py, benchmark/__main__.py, benchmark/datasets/*, tests/benchmark/test_cli.py, test_prepare_external_datasets.py, scripts/benchmark/jitter_baseline.py
实现 run/compare CLI、公开数据集下载注册表与转换脚本,以及 baseline 演示扰动脚本。

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI
  participant Runner
  participant MetricRegistry
  participant LLMJudge
  participant BaselineStore
  participant MarkdownReporter

  User->>CLI: run --mode ...
  CLI->>Runner: run(data_path, metrics)
  Runner->>MetricRegistry: create(metric_name)
  Runner->>LLMJudge: judge(...)  %% 可选
  LLMJudge-->>Runner: score/reason
  Runner-->>CLI: BenchmarkResult
  CLI->>BaselineStore: save(result)  %% 可选
  CLI->>MarkdownReporter: report(result, comparison)
  MarkdownReporter-->>User: Markdown / JSON
Loading

小兔子搬来新木牌,🐇
指标、Runner 和报告排成排;
baseline 轻轻存起来,
中英文样例都到位,
一敲命令行,结果蹦出来。

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题简洁且准确概括了这次新增轻量级 GraphRAG benchmark 的核心变化。
Linked Issues check ✅ Passed PR 覆盖了 #75 要求的 CLI、图抽取与召回评测、baseline 对比、JSON/Markdown 报告、中文/英文样例和样例级失败展示。
Out of Scope Changes check ✅ Passed 未见明显与 #75 目标无关的代码变更,新增内容基本都围绕 benchmark、数据准备、报告和测试展开。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Postroggy
Postroggy force-pushed the feat/graphrag-benchmark-issue7 branch from 22a4877 to fbb0823 Compare July 1, 2026 18:25

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive GraphRAG Benchmark evaluation suite to the hugegraph-llm project, adding various metrics for graph extraction, document retrieval, and answer generation, along with orchestration runners, CLI commands, and extensive tests. The code review identified several critical bugs and improvement opportunities, including a matching bug in symmetric relation conflict detection due to underscore normalization, incorrect repository root fallback paths in shell scripts, a too-permissive Unix timestamp check in temporal validity, and a missing question-type re-computation when filtering samples in the CLI. Additionally, optimizations were suggested for memory reuse in ROUGE-L calculation and cleaner string representation of reference lists in context precision.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/rouge_l.py Outdated
Comment thread hugegraph-llm/src/tests/benchmark/test_conflict_detection.py Outdated
Comment thread hugegraph-llm/scripts/benchmark/run_external_benchmarks.sh Outdated
Comment thread hugegraph-llm/scripts/benchmark/run_small_datasets_experiment.sh Outdated
Comment thread hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/temporal_validity.py Outdated
Comment thread hugegraph-llm/src/hugegraph_llm/benchmark/cli.py
@Postroggy
Postroggy force-pushed the feat/graphrag-benchmark-issue7 branch from fbb0823 to d178b1e Compare July 2, 2026 02:24
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/hugegraph/hugegraph-ai/issues/comments/4858873296","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hugegraph/hugegraph-ai/pull/77?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Organization UI\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `689dac7d-0517-434d-abb8-7015680caf80`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between 4c643b0298cf50d86fe6cbe986d758a801b11111 and d178b1e6523bb1b92cdf1335cfdebb667042a26a.\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (87)</summary>\n> \n> * `.gitignore`\n> * `docs/quality/benchmark-code-style-spec.md`\n> * `hugegraph-llm/GRAPHRAG_BENCHMARK.md`\n> * `hugegraph-llm/pyproject.toml`\n> * `hugegraph-llm/scripts/benchmark/README.md`\n> * `hugegraph-llm/scripts/benchmark/run_external_benchmarks.sh`\n> * `hugegraph-llm/scripts/benchmark/run_hotpotqa_llm_demo.py`\n> * `hugegraph-llm/scripts/benchmark/run_hotpotqa_vector_demo.py`\n> * `hugegraph-llm/scripts/benchmark/run_small_datasets_experiment.sh`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/__main__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/baseline/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/baseline/compare.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/baseline/store.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/cli.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/ablation_sample.json`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/car_extraction_sample.json`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/chinese_retrieval_sample.json`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/extraction_sample.json`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/retrieval_sample.json`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/datasets/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/datasets/prepare_external_datasets.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/base.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/judge_utils.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/llm_judge.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/mock_judge.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/prompts.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/answer_correctness.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/coverage.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/exact_match.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/faithfulness.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/rouge_l.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/token_f1.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/base.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/conflict_detection.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/entity_f1.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/graph_structure.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/property_f1.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/schema_validity.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/structural_integrity.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/syntax_validity.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/temporal_validity.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/triple_f1.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/registry.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/context_precision.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/context_relevancy.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/evidence_recall.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/hit_at_k.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/mrr.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/recall_at_k.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/models/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/models/result.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/reporters/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/reporters/json_reporter.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/reporters/markdown_reporter.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/runners/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/runners/ablation_runner.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/runners/base_runner.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/runners/extraction_runner.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/runners/retrieval_runner.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/utils/__init__.py`\n> * `hugegraph-llm/src/hugegraph_llm/benchmark/utils/normalize.py`\n> * `hugegraph-llm/src/tests/benchmark/__init__.py`\n> * `hugegraph-llm/src/tests/benchmark/test_answer_metrics.py`\n> * `hugegraph-llm/src/tests/benchmark/test_base_runner.py`\n> * `hugegraph-llm/src/tests/benchmark/test_baseline.py`\n> * `hugegraph-llm/src/tests/benchmark/test_cli.py`\n> * `hugegraph-llm/src/tests/benchmark/test_conflict_detection.py`\n> * `hugegraph-llm/src/tests/benchmark/test_e2e_car_dataset.py`\n> * `hugegraph-llm/src/tests/benchmark/test_e2e_cli.py`\n> * `hugegraph-llm/src/tests/benchmark/test_extraction_metrics.py`\n> * `hugegraph-llm/src/tests/benchmark/test_graph_structure.py`\n> * `hugegraph-llm/src/tests/benchmark/test_integration_ablation.py`\n> * `hugegraph-llm/src/tests/benchmark/test_integration_extraction.py`\n> * `hugegraph-llm/src/tests/benchmark/test_integration_retrieval.py`\n> * `hugegraph-llm/src/tests/benchmark/test_json_parse_utils.py`\n> * `hugegraph-llm/src/tests/benchmark/test_llm_judge_metrics.py`\n> * `hugegraph-llm/src/tests/benchmark/test_prepare_external_datasets.py`\n> * `hugegraph-llm/src/tests/benchmark/test_registry_fix.py`\n> * `hugegraph-llm/src/tests/benchmark/test_reproducibility.py`\n> * `hugegraph-llm/src/tests/benchmark/test_retrieval_metrics.py`\n> * `hugegraph-llm/src/tests/benchmark/test_temporal_validity.py`\n> \n> </details>\n> \n> ```ascii\n>  __________________________________________________________\n> < This abstraction leaks more than a sieve in a rainstorm. >\n>  ----------------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=hugegraph/hugegraph-ai&utm_content=77)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@Postroggy
Postroggy force-pushed the feat/graphrag-benchmark-issue7 branch from d178b1e to d789fbc Compare July 2, 2026 02:50
@Postroggy
Postroggy force-pushed the feat/graphrag-benchmark-issue7 branch from d789fbc to 801db09 Compare July 2, 2026 03:23

@imbajin imbajin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗️ 总体结论:建议先 Request changes,暂不建议直接合并。综合评分:6.0 / 10。

方向是对的:把 GraphRAG benchmark 做成 hugegraph_llm.benchmark 下相对独立、离线优先、可用于 PR 回归对比的工具,这个目标很适合 HugeGraph-LLM 的工程场景。但当前 PR 的核心问题不是“指标数量不够”,而是 benchmark 数据契约和 metric 输入边界没有收敛。在 benchmark 领域,最重要的是分数可信、可解释、可复现;如果契约混用,report 越完整越容易误导用户。

当前设计里有三类概念被混在一起:

flowchart LR
    A[retrieved_docs / gold_docs<br/>doc id 列表] --> B[离线 ranking metrics<br/>recall@k / hit@k / mrr]
    A -. 当前也传给 .-> C[LLM Judge metrics<br/>context_precision / evidence_recall]
    D[retrieved_contexts<br/>文本片段] --> C
    E[gold_evidence / gold_answer<br/>证据与答案文本] --> C
Loading

建议把首版目标收窄到一个更稳的最小闭环:

数据格式清晰  ->  metric 输入明确  ->  分数方向明确  ->  compare 不误判  ->  report 可解释

合并前建议至少完成:

  1. ❗️ 修复 graph_structure 中顶点 ID 与边端点 ID 不一致导致的拓扑指标错误。
  2. ❗️ 明确 retrieval 数据契约:doc id、context text、gold evidence、gold answer 需要分字段,不要让 LLM metric 吃 doc id。
  3. ❗️ 给 metric 增加 higher_is_better / direction 元信息,修复 baseline compare 对 error/rate 指标的反向判断。
  4. ⚠️ CLI 文档与实现对齐:baseline save / list-metrics 要么实现,要么从文档删除。
  5. ⚠️ invalid metric、mode/data 不匹配、offline 下显式请求 LLM metric 都需要 fail-fast 或显式 skipped_metrics
  6. ⚠️ 补关键回归测试,而不只是 toy fixture 的“能跑”。

已有评论线程中 resolved/outdated 的问题我这里不重复。下面 inline comments 主要聚焦仍然影响设计、执行正确性、用户易用性和测试有效性的点。

in_v = str(_edge_in(e))
edge_label = str(e.get("label", ""))
if out_v and in_v:
g.add_edge(out_v, in_v, label=edge_label)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗️ 这里的节点 ID 和边端点 ID 使用了两套规则,会系统性算错图拓扑指标。

上面添加节点时,如果 vertex 有 label,会使用 label:name

vertex: {label: person, name: Alice} -> node_id = person:Alice

但这里添加边时直接使用 _edge_out/_edge_in 返回的裸端点:

edge: {source: Alice, target: Bob} -> Alice -> Bob

NetworkX 会自动把 Alice / Bob 当成新节点,导致一个正常的 2 点 1 边图变成:

{ person:Alice, person:Bob, Alice, Bob }

这样 num_nodesdensitynum_componentslargest_component_ratio 都会被污染。建议抽一个统一的 endpoint canonicalization helper,例如先构建 name -> canonical_node_id 映射,边端点统一映射到同一套 node id。

sample_id=sample_id,
question=sample.get("question", ""),
context=sample.get("retrieved_docs", []),
ground_truth=sample.get("gold_answer", sample.get("gold_docs", [])),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗️ 这里把 retrieved_docs / gold_docs 同时传给所有 retrieval metrics,导致 doc-id 指标和 LLM/context 指标的输入契约混在一起。

recall@k / hit@k / mrr 吃 doc id 没问题;但 context_precisioncontext_relevancyevidence_recall_llm 需要的是 context text / evidence text。当前如果 sample 只有 doc_paris 这类 id,LLM metric 会把 doc id 当上下文或证据来评估,得到的是“能跑”的分数,不是可信分数。

建议数据结构拆开:

{
  "gold_doc_ids": ["doc_a"],
  "retrieved_doc_ids": ["doc_a", "doc_b"],
  "retrieved_contexts": [{"id": "doc_a", "text": "..."}],
  "gold_evidence": ["..."],
  "gold_answer": "..."
}

runner 按 metric 类型选择输入;缺少文本时应显式 skip LLM metric,而不是默默把 doc id 传进去。

if _is_llm_judge_metric(metric):
effective_delta = max(delta, cls.DEFAULT_LLM_JUDGE_DELTA)

if diff < -effective_delta:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗️ baseline compare 需要知道每个 metric 的方向,否则会把低越好的指标反向解释。

当前统一用 candidate - baseline

正数 => improvement
负数 => regression

这对 f1/recall/precision 成立,但对 illegal_edge_rateorphan_edge_rateduplicate_entity_rateduplicate_edge_rateconflict_ratenum_conflicts 这类 error/rate 指标是反的。例如:

illegal_edge_rate: 0.10 -> 0.30
当前逻辑:+0.20 => improvement
实际语义:更差,应该是 regression

建议给 metric 加一个极简元信息即可,不需要上复杂框架:

higher_is_better: bool = True

compare 时按方向归一化 delta,再判断 regression / improvement。

metrics so they can be selected explicitly, e.g. ``--metrics coverage``.
"""
allowed = set(_MODE_ALLOWED_METRICS.get(mode_key, []))
return [m for m in metrics if m in allowed]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 这里静默过滤 metric 会让错误输入变成“成功但无意义”的 benchmark。

如果用户写错 metric 名,或把 extraction metric 传给 retrieval mode,当前会被直接 drop;极端情况下 metrics 为空,runner 仍可能输出一个看似成功的 report。benchmark 工具更应该 fail-fast,避免用户在 PR 里贴出空结果或不完整结果。

建议:

requested_metrics - allowed_metrics != empty -> exit 2 + 打印非法项和可选项
filtered_metrics == empty -> exit 2

这样更符合工程回归工具的预期。

vl_name = normalize_answer(str(v.get("label", "")), language)
if vl_name not in vertex_labels_schema:
continue
primary_keys = vertex_labels_schema[vl_name].get("primary_keys", [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ required_property_fill 只检查 primary_keys,但样例 schema 使用的是 properties,没有 primary_keys。这会让“required property completeness”在当前样例和很多常见 schema 输入下偏乐观。

建议明确 schema contract:

required 字段来自哪里?
- primary_keys?
- nullable_keys 的反集?
- benchmark 自定义 required_properties?

不要把 properties 写在样例里,但实际不参与完整性校验;否则用户会以为缺少 name/age 会被扣分,实际不会。


return round(sum(scores) / len(scores))


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 双评估后这里直接 round() 会把 0.5 / 1.5 这类中间分压成整数,丢掉 dual-rating 的细粒度信息。

如果目标是降低 LLM 方差,建议保留平均值再归一化:

return sum(scores) / len(scores)

最终 mean_score / 2.0 输出即可。

answer_key = f"{mode}_answer"
prediction = sample.get(answer_key, "")

for metric_name, metric in metric_instances.items():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 缺失某个 answer mode 时默认当空答案计分,容易把数据准备问题混进模型质量分数。

例如某个 sample 没有 graph_vector_answer,当前会按 "" 继续计算,最后看起来像 graph+vector 表现很差,但真实原因可能只是数据缺字段。建议缺失字段时显式记录 sample error / skipped mode,或者在数据加载阶段校验必需字段。

@@ -0,0 +1,22 @@
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗️ 文件级:当前 retrieval sample 只有 doc id,没有 retrieved_contextsgold_evidencegold_answer。这与 PR 描述里的 context/evidence LLM-Judge 指标不匹配。

建议把样例拆成两类,避免用户误解:

retrieval_docid_sample.json
  - gold_doc_ids
  - retrieved_doc_ids
  - 只跑 recall@k / hit@k / mrr

retrieval_context_sample.json
  - question
  - retrieved_contexts[].text
  - gold_evidence[]
  - gold_answer
  - 可跑 context_precision / context_relevancy / evidence_recall_llm

这样既保持离线路径简洁,也不会让 LLM metric 在 doc id 上“假跑通”。

@@ -0,0 +1,115 @@
# Licensed to the Apache Software Foundation (ASF) under one

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 文件级:这里需要补一个关键回归测试,覆盖“带 label 顶点 + 裸 source/target 边端点”的真实样例形态。

建议直接加:

def test_graphstructure_labeled_vertices_source_target_edges_no_extra_nodes():
    prediction = {
        "vertices": [
            {"label": "person", "properties": {"name": "Alice"}},
            {"label": "person", "properties": {"name": "Bob"}},
        ],
        "edges": [{"label": "knows", "source": "Alice", "target": "Bob"}],
    }
    result = GraphStructure().calculate(prediction)
    assert result["num_nodes"] == 2.0
    assert result["num_edges"] == 1.0

当前测试里有边的 case 都是无 label 顶点,无法发现 person:AliceAlice 被当作两个节点的问题。

@@ -0,0 +1,554 @@
# Licensed to the Apache Software Foundation (ASF) under one

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 文件级:PR 描述和文档提到 baseline savelist-metrics,但当前 parser 只注册了 runcompare。这会造成用户按 README/PR 描述执行命令时直接失败。

建议二选一:

方案 A:实现命令
  hugegraph-benchmark baseline save ...
  hugegraph-benchmark list-metrics

方案 B:收敛文档
  统一改成 run --save-baseline
  删除 list-metrics 相关描述

首版为了保持轻量,我更倾向先做方案 B;如果保留 list-metrics,也建议输出 requires_llmhigher_is_betterrequired_fields,帮助用户理解指标契约。

Postroggy added 9 commits July 5, 2026 16:25
…egistry

- Remove fallback LLM client in benchmark CLI; use internal OpenAI-compatible

  client with fixed temperature=0.0 and seed=42 for LLM-Judge metrics.

- Record model/temperature/seed in BenchmarkResult.metadata and persist via

  BaselineStore.save.

- Move metric direction metadata into metric classes; query via

  MetricRegistry.is_higher_is_better() in baseline comparator.

- Update sample data, dataset converters, and runners for retrieval/extraction

  fields and answer runner.

- Add/expand unit tests for CLI judge params and baseline metadata.

Local infrastructure adjustments (kept local, not for upstream):

- Add Jina reranker option and robust schema JSON parsing.

- Add retry/rate-limit/timeout handling in OpenAI embedding and LLM clients.
…-ID validation

- Implement normalize_graph_extract() to convert property_graph / triples
  extraction output into benchmark candidate_vertices / candidate_edges format.
- Expose normalize_graph_extract from benchmark.utils.
- Improve RetrievalRunner error messages when ranking metrics are requested
  but gold_doc_ids / retrieved_doc_ids are missing; fail-fast with guidance
  to use context / LLM-Judge metrics instead.
- Add unit tests for graph extraction normalization and retrieval runner
  doc-ID contract validation.
…upstream)

Stash local-only artifacts so they are not lost while benchmark module
source stays on the PR branch. These changes are intended to stay local
and must NOT be pushed upstream:

- docs/benchmark/ experiment notes
- scripts/benchmark/ one-off generation/preparation scripts
- BENCHMARK_DATASETS.md / GRAPHRAG_BENCHMARK.md
- temporary changes to graph_extract flow / operators / ai_state / tests
  used to run experiments
- Add normalize_schema() to accept JSON-string schema from pipeline output
  and return the dict expected by ExtractionRunner.
- Add normalize_extraction_output() as the pipeline -> benchmark adapter for
  extraction: normalizes schema, converts vertices/edges to candidate fields,
  and preserves trace fields (raw_responses, parse_results) when
  collect_trace=True is enabled locally.
- Expose both helpers from benchmark.utils.
- Add unit tests for schema parsing and full extraction output normalization.
- Add build_retrieval_sample_from_state() to extract retrieved_contexts
  from WkFlowState.vector_result / graph_result for benchmark context
  and LLM-Judge metrics.
- Support raw / vector_only / graph_only / graph_vector modes matching
  the four RAG flows.
- Expose the adapter from benchmark.utils.
- Add unit tests covering all modes, JSON-string input, and field mapping.
- Add a 'Failed Samples' section to the single-run Markdown report using
  runner-collected errors from result.metadata.
- Add a 'Degraded Samples' section for single-run reports, listing samples
  whose metrics are at the worst bound (0 for higher-is-better, 1 for
  lower-is-better) or are None.
- Use MetricRegistry direction metadata so lower-is-better metrics are not
  falsely flagged.
- Add unit tests for failure/degradation reporting and error truncation.
- Restrict single-run degraded sample detection to primary quality metrics
  per mode, avoiding noise from auxiliary counters (e.g. num_temporal_attrs,
  clustering_coefficient, load_to_db_success).
- Extraction: entity_f1 / triple_f1 / property_f1.
- Retrieval: recall@* / hit_*@* / mrr / context precision / relevancy /
  evidence recall.
- Ablation/answer: token_f1 / exact_match / rouge_l / answer_correctness /
  faithfulness / coverage.
- Use 0.5 as degradation threshold and respect metric direction.
- Update tests to cover extraction/retrieval/unknown mode behavior.
- Clarify that single-run reports flag low-performing samples using a
  0.5 threshold on primary quality metrics, not true regression.
- Rename section to 'Low-performing Samples' and add an explanatory note
  pointing users to the Version: ImageMagick 7.1.2-26 Q16-HDRI aarch64 702175ac4:20260621 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/license/
Features: Cipher DPC HDRI Modules
Delegates (built-in): bzlib freetype heic jng jpeg lcms ltdl lzma png tiff webp xml zlib zstd
Compiler: clang (21.0.0)
Usage: compare [options ...] image reconstruct difference

Image Settings:
  -adjoin              join images into a single multi-image file
  -alpha option        on, activate, off, deactivate, set, opaque, copy
                       transparent, extract, background, or shape
  -authenticate password
                       decipher image with this password
  -background color    background color
  -colorspace type     alternate image colorspace
  -compose operator    set image composite operator
  -compress type       type of pixel compression when writing the image
  -decipher filename   convert cipher pixels to plain pixels
  -define format:option
                       define one or more image format options
  -density geometry    horizontal and vertical density of the image
  -depth value         image depth
  -dissimilarity-threshold value
                       maximum distortion for (sub)image match
  -encipher filename   convert plain pixels to cipher pixels
  -extract geometry    extract area from image
  -format "string"     output formatted image characteristics
  -fuzz distance       colors within this distance are considered equal
  -gravity type        horizontal and vertical text placement
  -highlight-color color
                       emphasize pixel differences with this color
  -identify            identify the format and characteristics of the image
  -interlace type      type of image interlacing scheme
  -limit type value    pixel cache resource limit
  -lowlight-color color
                       de-emphasize pixel differences with this color
  -metric type         measure differences between images with this metric
  -monitor             monitor progress
  -negate              replace every pixel with its complementary color
  -passphrase filename get the passphrase from this file
  -precision value     maximum number of significant digits to print
  -profile filename    add, delete, or apply an image profile
  -quality value       JPEG/MIFF/PNG compression level
  -quiet               suppress all warning messages
  -quantize colorspace reduce colors in this colorspace
  -read-mask filename  associate a read mask with the image
  -regard-warnings     pay attention to warning messages
  -respect-parentheses settings remain in effect until parenthesis boundary
  -sampling-factor geometry
                       horizontal and vertical sampling factor
  -seed value          seed a new sequence of pseudo-random numbers
  -set attribute value set an image attribute
  -quality value       JPEG/MIFF/PNG compression level
  -repage geometry     size and location of an image canvas
  -similarity-threshold value
                       minimum distortion for (sub)image match
  -size geometry       width and height of image
  -subimage-search     search for subimage
  -synchronize         synchronize image to storage device
  -taint               declare the image as modified
  -transparent-color color
                       transparent color
  -type type           image type
  -verbose             print detailed information about the image
  -version             print version information
  -virtual-pixel method
                       virtual pixel access method
  -write-mask filename  associate a write mask with the image

Image Operators:
  -auto-orient         automagically orient (rotate) image
  -brightness-contrast geometry
                       improve brightness / contrast of the image
  -distort method args
                       distort images according to given method and args
  -level value         adjust the level of image contrast
  -resize geometry     resize the image
  -rotate degrees      apply Paeth rotation to the image
  -sigmoidal-contrast geometry
                       increase the contrast without saturating highlights or
  -trim                trim image edges
  -write filename      write images to this file

Image Channel Operators:
  -separate            separate an image channel into a grayscale image

Image Sequence Operators:
  -crop geometry       cut out a rectangular region of the image

Image Stack Operators:
  -delete indexes      delete the image from the image sequence

Miscellaneous Options:
  -channel mask        set the image channel mask
  -debug events        display copious debugging information
  -help                print program options
  -list type           print a list of supported option arguments
  -log format          format of debugging information

By default, the image format of 'file' is determined by its magic
number.  To specify a particular image format, precede the filename
with an image format name and a colon (i.e. ps:image) or specify the
image type as the filename suffix (i.e. image.ps).  Specify 'file' as
'-' for standard input or output. command for baseline-based regression.
- Update internal helpers and tests accordingly.
…ic direction indicators

- MarkdownReporter no longer reports 'Low-performing Samples' in single-run
  mode because degradation requires a baseline.
- Add ↑/↓ direction symbols to Overall Metrics, Metrics by Question Type,
  and Regressed Samples tables using MetricRegistry.is_higher_is_better().
- Update tests to cover direction arrows and removal of low-performing logic.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
hugegraph-llm/src/tests/benchmark/test_markdown_reporter.py (1)

1-134: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

该文件未通过 ruff format --check,CI 已在 3.10/3.11/3.12 三个构建中报错。

请在合入前本地运行 uv run ruff format . 并重新提交,避免阻塞流水线。

As per coding guidelines, "For Python code changes, run root uv run ruff format --check . and uv run ruff check . before handoff."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/tests/benchmark/test_markdown_reporter.py` around lines 1 -
134, The test file is failing Ruff formatting, so reformat the module and verify
it passes style checks before handoff. Run formatting on the repository, then
recheck the test module around the MarkdownReporter tests to ensure imports,
long assertions, and multiline argument blocks match the formatter’s output and
the file no longer changes under format check.

Sources: Coding guidelines, Pipeline failures

♻️ Duplicate comments (1)
hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/schema_validity.py (1)

130-153: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

required_property_fill 的 schema 契约仍不明确,与历史评审意见重复。

当顶点标签 schema 未定义 primary_keys(而是用 properties 定义字段)时,primary_keys 为空列表,代码直接记为“完整性满分”,即便实际应有的属性(如 name/age)缺失也不会被扣分。此问题此前已被指出,建议明确 required 字段的来源(primary_keysnullable_keys 的反集,还是 benchmark 自定义的 required_properties),并在样例 schema 与文档中保持一致。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/schema_validity.py`
around lines 130 - 153, `required_property_fill` currently treats labels with
empty `primary_keys` as automatically complete, which hides missing required
fields when the schema defines properties but not keys. Update the logic in
`schema_validity.py` to derive required fields from an explicit contract in
`vertex_labels_schema` (for example `primary_keys`, a benchmark-defined
`required_properties`, or another clearly documented source) instead of
defaulting to full credit, and make the metric check those fields consistently
in the `required_property_fill` calculation. Ensure the schema sample and any
related docs/tests use the same required-field convention so the behavior is
unambiguous.
🟡 Minor comments (22)
hugegraph-llm/src/hugegraph_llm/benchmark/baseline/compare.py-104-146 (1)

104-146: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

指标单侧缺失时用 0.0 兜底可能掩盖回归。

当某个 lower-is-better 指标在 candidate 中缺失(如指标被移除或计算异常导致未产出),cand_val 默认 0.0,diff = -(0-base) = base > 0,会被误判为 improvement,而不是提示"指标缺失"。建议区分"缺失"与"值为 0"两种情况,例如对单侧缺失的指标单独记录警告而非直接参与方向计算。

💡 参考修复思路
-            for metric in sample_metrics:
-                base_val = base_sample.metrics.get(metric, 0.0)
-                cand_val = cand_sample.metrics.get(metric, 0.0)
-                diff = _semantic_delta(metric, base_val, cand_val)
+            for metric in sample_metrics:
+                if metric not in base_sample.metrics or metric not in cand_sample.metrics:
+                    # metric only present on one side: skip direction-based comparison
+                    # and optionally record as a distinct "metric_missing" signal.
+                    continue
+                base_val = base_sample.metrics[metric]
+                cand_val = cand_sample.metrics[metric]
+                diff = _semantic_delta(metric, base_val, cand_val)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/baseline/compare.py` around lines
104 - 146, In `compare_benchmark_results` (the loop building
`result.overall_diff` and per-sample `regressions`/`improvements`), missing
metrics are being treated as `0.0`, which can turn a removed or failed metric
into a fake improvement. Update the logic so `_semantic_delta` is only used when
both sides actually contain the metric; when `baseline.overall`,
`candidate.overall`, `base_sample.metrics`, or `cand_sample.metrics` lacks a
key, record it separately as a missing-metric warning/status instead of
comparing against zero. Keep the existing direction-aware diff only for real
values, and use the same handling for both overall and sample-level comparison
paths.
hugegraph-llm/src/hugegraph_llm/benchmark/models/result.py-85-101 (1)

85-101: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

skipped_metrics 在多次 compute_overall() 调用间会重复累积。

self.overall 每次调用都会重置(Line 87),但 self.metadata["skipped_metrics"] 只会 extend,从不清空。cli.py--smoke/--samples 场景下会对同一个 BenchmarkResult 多次调用 compute_overall()(先由 runner 计算一次,再在样本过滤后重算),导致同一 metric 名称在 skipped_metrics 中重复出现,甚至可能残留过滤前才成立、过滤后已不适用的过期条目,误导报告读者。

🐛 建议修复
     def compute_overall(self) -> None:
         """Compute overall metrics by averaging per-sample metrics."""
         self.overall = {}
+        self.metadata["skipped_metrics"] = []
         if not self.samples:
             return
         all_keys: set = set()
         for s in self.samples:
             all_keys.update(s.metrics.keys())
         skipped: List[str] = []
         for key in all_keys:
             values = [s.metrics[key] for s in self.samples if key in s.metrics and s.metrics[key] is not None]
             if values:
                 self.overall[key] = round(sum(values) / len(values), 4)
             else:
                 skipped.append(key)
         if skipped:
-            self.metadata.setdefault("skipped_metrics", []).extend(skipped)
+            self.metadata["skipped_metrics"] = skipped
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/models/result.py` around lines 85 -
101, `compute_overall` in `BenchmarkResult` currently resets `overall` but keeps
appending to `metadata["skipped_metrics"]`, so repeated calls accumulate
duplicates and stale entries. Update `compute_overall` to clear or recompute
`skipped_metrics` on each invocation before extending it, and ensure the logic
in `BenchmarkResult.compute_overall` only reflects the current `samples` state
when `cli.py` triggers multiple recomputations.
hugegraph-llm/scripts/benchmark/run_external_benchmarks.sh-43-65 (1)

43-65: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

单个数据集失败会导致后续所有数据集被跳过。

set -euo pipefail 下,run_retrieval/run_extraction 内部调用 "${BENCHMARK[@]}" 一旦非零退出,脚本立即终止,line 71-76、81 中尚未执行的数据集将不会被跑到,这与"跑遍所有已准备好的外部数据集"的烟雾测试目的相悖。

💡 建议:捕获单次运行的失败并继续执行剩余数据集
+FAILED=0
+
 run_retrieval() {
     local name="$1"
     local lang="$2"
     local file="$DATA_DIR/${name}_retrieval.json"
     if [[ ! -f "$file" ]]; then
         echo "SKIP: $file not found"
         return
     fi
     echo "==> Running retrieval benchmark: $name"
-    "${BENCHMARK[@]}" --mode retrieval --data "$file" --language "$lang" --offline
+    if ! "${BENCHMARK[@]}" --mode retrieval --data "$file" --language "$lang" --offline; then
+        echo "FAILED: $name"
+        FAILED=1
+    fi
     echo ""
 }

同样处理 run_extraction,并在脚本末尾 exit $FAILED

Also applies to: 70-83

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/run_external_benchmarks.sh` around lines 43 -
65, The benchmark helpers run under set -euo pipefail, so a non-zero exit from
BENCHMARK inside run_retrieval or run_extraction aborts the whole script and
skips remaining datasets. Update run_retrieval and run_extraction to catch each
"${BENCHMARK[@]}" failure, record it in a shared FAILED flag, and continue
looping through the rest of the datasets; then have the script exit with that
accumulated status at the end. Use the run_retrieval and run_extraction
functions and the final summary/exit path as the main places to adjust.
hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/coverage.py-103-170 (1)

103-170: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

补充 Coverage 单测
hugegraph-llm/src/tests/benchmark/test_answer_metrics.pytest_llm_judge_metrics.py 里还没有 Coverage 的用例。建议补上正常路径,以及 llm 缺失、空 reference、fact extraction 失败等分支的测试,避免这个新指标后续回归。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/coverage.py` around
lines 103 - 170, Coverage metric tests are missing, so add coverage cases to the
existing answer-metrics and judge-metrics test suites by targeting the
Coverage.calculate method and its helpers _extract_facts and _check_coverage.
Cover the normal happy path, missing llm returning None fields, empty reference
yielding perfect coverage, and fact extraction failure returning None, and
assert the returned keys coverage, coverage_ref_facts, and coverage_covered
match the expected branch behavior.

Source: Path instructions

hugegraph-llm/src/hugegraph_llm/models/llms/openai.py-47-47 (1)

47-47: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

OPENAI_TIMEOUT 格式错误会导致启动崩溃。

若环境变量 OPENAI_TIMEOUT 被设置为非数字字符串,float(...) 会抛出 ValueError,在客户端构造阶段直接崩溃而非优雅降级。

🛡️ 建议修复
-        timeout = float(os.getenv("OPENAI_TIMEOUT", "0")) or None
+        try:
+            timeout = float(os.getenv("OPENAI_TIMEOUT", "0")) or None
+        except ValueError:
+            log.warning("Invalid OPENAI_TIMEOUT value; falling back to no explicit timeout")
+            timeout = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/models/llms/openai.py` at line 47, The
OPENAI_TIMEOUT parsing in the OpenAI client setup can crash startup when the env
var is not numeric. Update the timeout handling in the openai.py initialization
path to safely parse OPENAI_TIMEOUT with error handling, and fall back to a
default/None value instead of letting ValueError escape. Use the existing
timeout assignment near the OpenAI client construction as the fix point.
hugegraph-llm/src/hugegraph_llm/operators/llm_op/schema_build.py-76-115 (1)

76-115: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

补充 SchemaBuilder._extract_schema 的单测:当前没有针对 hugegraph_llm/operators/llm_op/schema_build.py 的直接测试,建议补上未闭合围栏、前缀/尾部噪声和数组根节点的用例,避免这段新增解析逻辑回归。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/operators/llm_op/schema_build.py` around
lines 76 - 115, Add direct unit tests for SchemaBuilder._extract_schema to cover
the new parsing paths: a truncated or unclosed fenced JSON block, responses with
explanatory text before and/or after the JSON, and a valid array-root JSON
payload. Use the _extract_schema static method from SchemaBuilder in
schema_build.py so the tests stay focused on this logic and guard against
regressions in the added normalization and trimming behavior.

Source: Path instructions

hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/conflict_detection.py-53-58 (1)

53-58: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

实体身份应包含 label

_detect_property_conflicts 现在只按归一化后的 name 分组,label 没有参与;而上游顶点数据本身保留了 label。同名异类实体(如 Person:AliceCompany:Alice)会被合并,进而产生虚假的属性冲突。建议把实体键改成 (label, name) 或等价的复合键。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/conflict_detection.py`
around lines 53 - 58, The conflict grouping currently uses only the normalized
vertex name, so different entity types with the same name can be merged
incorrectly. Update the identity key used by _detect_property_conflicts to
include the vertex label together with the name, and adjust any helper such as
_get_vertex_name or a new key builder so vertices are grouped by a composite
(label, name) identity instead of name alone.
hugegraph-llm/src/hugegraph_llm/benchmark/runners/ablation_runner.py-32-37 (1)

32-37: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

_validate_sample_contract 未校验 sample_id,导致缺失时抛出裸 KeyError 而非清晰的 ValueError。

required_fields(第34行)未包含 sample_id,但第101行 sample["sample_id"] 直接索引访问。若样本缺失 sample_id,将绕过契约校验、在 process_sample 内抛出 KeyError,而不是本函数设计的、更具诊断价值的 ValueError 提示,弱化了 fail-fast 契约校验的一致性。

🐛 建议修复
 def _validate_sample_contract(sample: Dict[str, Any]) -> None:
     sample_id = sample.get("sample_id", "unknown")
-    required_fields = ["gold_answer", *[f"{mode}_answer" for mode in _ANSWER_MODES]]
+    required_fields = ["sample_id", "gold_answer", *[f"{mode}_answer" for mode in _ANSWER_MODES]]
     missing = [field for field in required_fields if field not in sample]
     if missing:
         raise ValueError(f"Ablation sample {sample_id!r} missing required field(s): {', '.join(missing)}")

Also applies to: 100-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/runners/ablation_runner.py` around
lines 32 - 37, _validate_sample_contract currently skips checking sample_id, so
process_sample can fail later with a bare KeyError instead of a clear
ValueError. Add sample_id to the required_fields validation in
_validate_sample_contract and keep the existing missing-field error path so any
sample without sample_id is rejected up front with the same contract-checking
behavior as gold_answer and the answer fields.
hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/triple_f1.py-46-47 (1)

46-47: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"双空"约定与其他指标不一致。

predictionreference 均为空列表时,此处返回 triple_precision/recall/f1 = 0.0。而同一 PR 中的 token_f1.py(_compute_token_f1_single,双空返回 1.0)和 rouge_l.py(第 112-113 行,双空返回 1.0)都将"预测与参考皆为空"视为平凡匹配(1.0)。

若某条样本本身没有可抽取的三元组(gold 为空)且模型也正确地未产生三元组,当前实现会把这种"正确的空结果"记为 0 分,拉低总体指标,与项目其他指标的语义不一致。

🐛 建议修复
     if not prediction and not reference:
-        return {"triple_precision": 0.0, "triple_recall": 0.0, "triple_f1": 0.0}
+        return {"triple_precision": 1.0, "triple_recall": 1.0, "triple_f1": 1.0}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/triple_f1.py`
around lines 46 - 47, In triple_f1.py, the empty-empty case in the triple F1
computation is inconsistent with the project’s other metrics. Update the
empty-input branch in the triple F1 helper so that when both prediction and
reference are empty it returns 1.0 for triple_precision, triple_recall, and
triple_f1, matching the behavior used in token_f1.py and rouge_l.py. Locate the
logic in the triple F1 computation function handling the “prediction and
reference both empty” check and adjust that special case accordingly.
hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/structural_integrity.py-118-129 (1)

118-129: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

端点字段完全缺失的边未被计入 orphan_edge_rate

out_v and out_v not in vertex_namesout_v 为空字符串(即边完全没有 outV/source 字段)时短路为 False,导致这类边不会被判定为孤立边,只要另一端点命中已有顶点即可“蒙混过关”。这会低估该质量指标,掩盖抽取结果中端点信息缺失的问题。

🐛 建议修复
         if edges:
             orphan_count = 0
             for e in edges:
                 out_v = normalize_answer(str(_edge_out(e)), language)
                 in_v = normalize_answer(str(_edge_in(e)), language)
-                if out_v and out_v not in vertex_names:
-                    orphan_count += 1
-                elif in_v and in_v not in vertex_names:
-                    orphan_count += 1
+                if (not out_v or out_v not in vertex_names) or (
+                    not in_v or in_v not in vertex_names
+                ):
+                    orphan_count += 1
             orphan_edge_rate = orphan_count / len(edges)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/structural_integrity.py`
around lines 118 - 129, The orphan edge calculation in the structural integrity
metric is missing edges whose endpoint field is entirely absent, so these should
still count toward orphan_edge_rate. Update the logic in the orphan counting
loop in the metric function that computes orphan_edge_rate to treat a missing or
empty outV/source or inV/target as an orphan condition, rather than relying on
the current truthy check that short-circuits on empty strings. Make the check
explicit for the edge endpoint accessors such as _edge_out and _edge_in so edges
with one or both endpoints missing are correctly counted.
hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.py-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Ruff format 检查未通过。

CI 显示 ruff format --check 在多个 Python 版本下均报告该文件需要重新格式化,请在提交前本地运行 uv run ruff format .uv run ruff check .

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.py` at line
1, The file is failing Ruff formatting checks, so reformat the Python script to
match the repository style before committing. Use the script entry point in
generate_text2kgbench_candidates.py and run the project’s formatting and lint
commands locally, then ensure the file passes ruff format and ruff check without
relying on version-specific behavior.

Source: Pipeline failures

hugegraph-llm/scripts/benchmark/fix_car33_edge_ids.py-35-36 (1)

35-36: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

edge.get("outV", "") 无法处理显式 null 值。

若原始 JSON 中 outV/inV 字段存在但值为 nulldict.get(key, default) 会返回 None 而非默认值,随后 _strip_id_prefix(None) 会把 str(None) 即字符串 "None" 写入结果,产生错误数据而非空字符串。

🐛 建议修复
-        fixed_edge["outV"] = _strip_id_prefix(edge.get("outV", ""))
-        fixed_edge["inV"] = _strip_id_prefix(edge.get("inV", ""))
+        fixed_edge["outV"] = _strip_id_prefix(edge.get("outV") or "")
+        fixed_edge["inV"] = _strip_id_prefix(edge.get("inV") or "")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/fix_car33_edge_ids.py` around lines 35 - 36,
The edge ID normalization in fix_car33_edge_ids.py should handle explicit null
values for outV and inV instead of passing None into _strip_id_prefix. Update
the logic around the fixed_edge assignments to coalesce None to an empty string
before calling _strip_id_prefix, so fix_car33_edge_ids does not serialize "None"
into the output. Keep the change localized to the edge field handling for outV
and inV.
hugegraph-llm/src/hugegraph_llm/benchmark/runners/retrieval_runner.py-76-91 (1)

76-91: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

sample_id 处理在校验与执行阶段不一致。

_validate_sample_contract(Line 77)用 sample.get("sample_id", "unknown") 容忍 sample_id 缺失,但 process_sample(Line 167)用 sample["sample_id"] 强制下标访问。若样本缺少 sample_id,校验阶段不会报错,执行阶段却会抛 KeyError,虽然会被 _run_samples_concurrent 的线程异常兜底捕获,但错误信息会变成不清晰的 "__sample__" 类型,而不是校验阶段本该给出的明确提示。

💚 建议修复
-            sample_id = sample["sample_id"]
+            sample_id = sample.get("sample_id", "unknown")

Also applies to: 166-171

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/runners/retrieval_runner.py` around
lines 76 - 91, _validate_sample_contract and process_sample should use the same
sample_id handling so missing IDs are caught during validation instead of
failing later as a generic thread error. Update process_sample to avoid direct
sample["sample_id"] access and reuse the same safe lookup/validation pattern
used in _validate_sample_contract, so any missing sample_id produces a clear,
explicit error message before _run_samples_concurrent executes the sample.
hugegraph-llm/scripts/benchmark/run_small_datasets_experiment.sh-119-136 (1)

119-136: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

时间戳提取丢失日期部分。

exp_dir.name 形如 small_datasets_20260705_120000,split('_')[-1] 只取到 120000,报告中 Timestamp 字段会丢失日期,只剩时间。

💚 建议修复
-lines.append(f"- **Timestamp**: {exp_dir.name.split('_')[-1]}")
+lines.append(f"- **Timestamp**: {'_'.join(exp_dir.name.split('_')[-2:])}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/run_small_datasets_experiment.sh` around
lines 119 - 136, The Timestamp extraction in the benchmark report generation is
dropping the date because it only uses the last underscore segment from
exp_dir.name. Update the report-building logic near load_baseline/ fmt_metrics
so the Timestamp line uses the full timestamp portion from the experiment
directory name (e.g. the date and time embedded in small_datasets_*), instead of
only the HHMMSS part. Keep the change localized to the report header
construction where commit and Timestamp are appended.
hugegraph-llm/src/hugegraph_llm/benchmark/baseline/store.py-33-45 (1)

33-45: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

subprocess.run 添加超时保护

_get_git_commit 调用外部 git 进程但未设置 timeout,异常环境下(如 git 挂起等待输入)可能导致该调用无限期阻塞,进而拖慢 save()

Static analysis 报告的命令注入/路径遍历提示在此场景下(本地离线 CLI 工具,参数硬编码、路径为调用方本地传入)为误报,未采纳。

🛡️ 修复建议
             result = subprocess.run(
                 ["git", "rev-parse", "HEAD"],
                 capture_output=True,
                 text=True,
+                timeout=5,
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/baseline/store.py` around lines 33
- 45, The `_get_git_commit` helper currently runs `subprocess.run` without any
timeout, so a hung `git rev-parse HEAD` can block `save()` indefinitely. Update
`_get_git_commit` to pass a reasonable `timeout` to `subprocess.run`, and handle
the timeout case by falling back to "unknown" just like the existing exception
path. Keep the fix localized to `_get_git_commit` in `store.py`, and preserve
the current return behavior for successful and failed git lookups.
hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/graph_structure.py-52-70 (1)

52-70: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

残留的同名顶点 ID 冲突风险

之前审查中提到的"边端点未规范化"问题已通过 name_to_node_id + canonical_endpoint 修复。但 name_to_node_id 仅以 name_str 为 key(Line 63),未包含 label。若存在不同 label 但同名的顶点(例如人名 "Washington" 与地名 "Washington"),后处理的顶点会覆盖映射,导致边端点被错误规范化到错误节点,重新引入拓扑指标(num_nodes/density/num_components 等)计算偏差。

建议在冲突时保留 (label, name) 的精确匹配,或在遇到重名冲突时记录警告/跳过而非静默覆盖。

🛠️ 修复建议
     name_to_node_id: Dict[str, str] = {}
+    name_collision: Dict[str, bool] = {}
 
     # Add nodes
     for v in vertices:
         name = v.get("name")
         if not name and isinstance(v.get("properties"), dict):
             name = v["properties"].get("name", "")
         if name:
             label = str(v.get("label", ""))
             name_str = str(name)
             node_id = f"{label}:{name_str}" if label else name_str
-            name_to_node_id[name_str] = node_id
+            if name_str in name_to_node_id and name_to_node_id[name_str] != node_id:
+                name_collision[name_str] = True
+            name_to_node_id[name_str] = node_id
             g.add_node(node_id, label=label, name=name_str)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/graph_structure.py`
around lines 52 - 70, The current name_to_node_id mapping in graph_structure.py
can silently overwrite entries when different labels share the same name,
causing canonical_endpoint to resolve edges to the wrong node. Update the
add-nodes / canonical_endpoint flow so node lookup prefers an exact (label,
name) match or otherwise handles duplicate names safely, and avoid silent
overwrites in name_to_node_id. Use the existing canonical_endpoint helper and
g.add_node metadata to keep endpoint normalization consistent without breaking
topology metrics.
hugegraph-llm/src/hugegraph_llm/models/embeddings/openai.py-119-141 (1)

119-141: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

重试循环在最后一次失败后仍会 sleep,增加不必要的失败延迟

_embed_with_retry / _async_embed_with_retry 在最后一次尝试失败后仍执行 time.sleep(wait)(最长可达 60s)才抛出 RuntimeError,调用方本可以更快得到失败反馈。此外两个方法逻辑几乎完全重复,可考虑抽取公共部分。

⚡ 建议的修改
             except (RateLimitError, APIConnectionError, APITimeoutError) as exc:
                 last_exc = exc
-                wait = min(2 ** attempt, 60)
-                log.warning("Embedding request failed (attempt %d/%d): %s; retrying in %ds", attempt + 1, max_retries, exc, wait)
-                time.sleep(wait)
+                if attempt < max_retries - 1:
+                    wait = min(2 ** attempt, 60)
+                    log.warning("Embedding request failed (attempt %d/%d): %s; retrying in %ds", attempt + 1, max_retries, exc, wait)
+                    time.sleep(wait)
         raise RuntimeError(f"Embedding failed after {max_retries} retries: {last_exc}")

(async 版本同理,改用 await asyncio.sleep(wait))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/models/embeddings/openai.py` around lines 119
- 141, The retry helpers `_embed_with_retry` and `_async_embed_with_retry` still
sleep after the final failed attempt, which adds avoidable latency before
surfacing the error. Update the retry loop in both methods so the backoff sleep
only happens when another retry remains, and on the last attempt raise the
`RuntimeError` immediately after logging the failure. Keep the retry behavior
aligned between the sync and async paths (`time.sleep` vs `asyncio.sleep`) and
consider sharing the common retry flow if you touch both methods.
hugegraph-llm/scripts/benchmark/prepare_benchmark_subsets.py-66-87 (1)

66-87: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

分层抽样会静默丢弃小类别,与常规采样路径行为不一致

_stratified_sample 对每个 question_type 桶执行 k = max(1, int(len(bucket) * fraction)) if len(bucket) * fraction >= 1 else 0,当某个类型的样本数较少(如 MuSiQue 5% 场景下 <20 条)时,k 直接为 0,该类型会被完全排除在子集之外;而非分层路径(第 74-75 行)保证 k 至少为 1。这会导致分层抽样在小类别上出现覆盖缺口,与"按 question_type 分层"的设计初衷不符。

🐛 建议的修改
     for bucket in buckets.values():
-        k = max(1, int(len(bucket) * fraction)) if len(bucket) * fraction >= 1 else 0
-        if k > 0:
-            selected.extend(random.sample(bucket, k))
+        k = max(1, int(len(bucket) * fraction))
+        selected.extend(random.sample(bucket, min(k, len(bucket))))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/prepare_benchmark_subsets.py` around lines 66
- 87, The `_stratified_sample` logic in `prepare_benchmark_subsets.py` is
dropping small `question_type` buckets because it allows `k` to become 0 for
low-count groups. Update the bucket sampling in `_stratified_sample` so each
non-empty `question_type` bucket still contributes at least one item, matching
the behavior of the non-stratified path and preserving coverage for rare
categories. Use the existing `buckets` loop and `random.sample` flow, and adjust
only the per-bucket `k` calculation/shuffle path.
hugegraph-llm/BENCHMARK_DATASETS.md-650-657 (1)

650-657: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Extraction 结果表格全部是占位符 "—",建议补全或说明未完成原因。

第 8 节其余表格(Retrieval + Answer)都已填入真实跑测数值,但 Extraction 表格所有指标列均为空占位符,容易让读者误认为抽取评测已完成并得到有效结果。建议补充实际数值,或在表格上方明确注明"抽取评测尚未完成/数值待补充",避免误导。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/BENCHMARK_DATASETS.md` around lines 650 - 657, The Extraction
benchmark table in BENCHMARK_DATASETS.md is still filled with placeholder
dashes, unlike the other benchmark sections. Update the Extraction section by
either replacing the placeholders with the actual values in the relevant table
rows or adding a clear note near the table that the extraction evaluation is not
yet completed and the numbers are pending; use the existing “Extraction(离线 +
LLM-Judge)” table and its metric columns as the place to fix.
hugegraph-llm/src/tests/benchmark/test_integration_extraction.py-189-198 (1)

189-198: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

该测试未真正验证"错误被追踪"的行为。

bad_data 缺少 gold_vertices/candidate_vertices 等字段,但这些字段在 runner 中均有默认值兜底(sample.get(key, [])),实际不会触发错误路径;测试也只断言 error_count 键存在,未断言其值 > 0。这使得测试名与实际验证内容不符,无法在未来错误追踪逻辑被破坏时捕获回归。建议构造一个真正会导致 metric 抛异常的样本(如缺少 sample_id 或字段类型错误),并断言 error_count > 0

As per path instructions, "Any code change in hugegraph-llm must add or update tests that exercise the changed behavior, regression risk, or failure path."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/tests/benchmark/test_integration_extraction.py` around
lines 189 - 198, The test in
test_extractionrunnererrortracking_error_tracking_with_bad_sample is not
actually exercising the error-tracking path because the missing vertex fields
are defaulted by ExtractionRunner. Update the fixture in this test to use a
sample that truly makes the metric evaluation fail (for example, an invalid
sample shape or missing required identifier that causes the benchmark flow to
raise), and then assert not just that BenchmarkResult.metadata contains
error_count but that error_count is greater than zero. Keep the focus on the
run() path and the error aggregation behavior so the test name matches the
verified outcome.

Source: Path instructions

hugegraph-llm/src/hugegraph_llm/benchmark/datasets/download.py-79-82 (1)

79-82: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

force 未传递到 _derive_hotpotqa_corpus,导致强制刷新后 corpus 仍是旧数据。

download_dataset 对原始文件的下载遵循 force,但衍生的 hotpotqa_corpus.json 只要文件存在就直接跳过重新生成(Line 154-156),未考虑上游文件是否刚被强制刷新。对于强调可复现性的 benchmark 工具,这会导致 --force 之后语料与最新原始数据不一致。

🛠️ 建议修复
     if spec.postprocess == "hotpotqa_corpus":
         _derive_hotpotqa_corpus(
-            data_root / "hotpotqa" / "hotpotqa.json", data_root / "hotpotqa" / "hotpotqa_corpus.json"
+            data_root / "hotpotqa" / "hotpotqa.json", data_root / "hotpotqa" / "hotpotqa_corpus.json", force=force
         )
-def _derive_hotpotqa_corpus(qa_file: Path, corpus_file: Path) -> None:
-    if corpus_file.exists():
+def _derive_hotpotqa_corpus(qa_file: Path, corpus_file: Path, force: bool = False) -> None:
+    if corpus_file.exists() and not force:
         logger.info("Derived corpus already exists: %s", corpus_file)
         return

Also applies to: 153-157

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/datasets/download.py` around lines
79 - 82, The hotpotqa corpus regeneration path in
download_dataset/_derive_hotpotqa_corpus ignores force, so a forced refresh can
leave hotpotqa_corpus.json stale. Pass the force flag through the
hotpotqa_corpus branch and update _derive_hotpotqa_corpus to regenerate when
force is true even if the output file already exists, ensuring the derived
corpus stays in sync with freshly downloaded hotpotqa.json.
hugegraph-llm/src/hugegraph_llm/benchmark/datasets/registry.py-66-70 (1)

66-70: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

将 HotpotQA 下载地址改为 HTTPS

http://curtis.ml.cmu.edu/datasets/hotpot/hotpot_dev_distractor_v1.json 有可用的 https:// 版本,建议直接切换,避免明文传输导致基准数据被篡改、影响可复现性。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/datasets/registry.py` around lines
66 - 70, HotpotQA 的下载链接仍使用明文 HTTP,需要改为 HTTPS;请在 registry.py 中定位 HotpotQA 对应的
DownloadFile 配置,将该 URL 切换为可用的 https:// 版本,保持 path 和其它基准数据配置不变,以确保下载链路安全且可复现。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3503cc58-dcdf-464e-bab0-7e3ac8d2d9d6

📥 Commits

Reviewing files that changed from the base of the PR and between 4c643b0 and a0abdfb.

📒 Files selected for processing (120)
  • .gitignore
  • docs/quality/benchmark-code-style-spec.md
  • hugegraph-llm/BENCHMARK_DATASETS.md
  • hugegraph-llm/GRAPHRAG_BENCHMARK.md
  • hugegraph-llm/docs/benchmark/experiment-record.md
  • hugegraph-llm/docs/benchmark/experiment-report.md
  • hugegraph-llm/pyproject.toml
  • hugegraph-llm/scripts/benchmark/README.md
  • hugegraph-llm/scripts/benchmark/fix_car33_edge_ids.py
  • hugegraph-llm/scripts/benchmark/generate_hugegraph_retrieval_outputs.py
  • hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.py
  • hugegraph-llm/scripts/benchmark/prepare_benchmark_subsets.py
  • hugegraph-llm/scripts/benchmark/prepare_car33_benchmark.py
  • hugegraph-llm/scripts/benchmark/run_benchmarks.py
  • hugegraph-llm/scripts/benchmark/run_car33_pipeline_extraction.py
  • hugegraph-llm/scripts/benchmark/run_external_benchmarks.sh
  • hugegraph-llm/scripts/benchmark/run_hotpotqa_llm_demo.py
  • hugegraph-llm/scripts/benchmark/run_hotpotqa_vector_demo.py
  • hugegraph-llm/scripts/benchmark/run_small_datasets_experiment.sh
  • hugegraph-llm/scripts/benchmark/summarize_baselines.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/__main__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/baseline/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/baseline/compare.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/baseline/store.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/cli.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/ablation_sample.json
  • hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/car_extraction_sample.json
  • hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/chinese_retrieval_sample.json
  • hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/extraction_sample.json
  • hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/retrieval_context_sample.json
  • hugegraph-llm/src/hugegraph_llm/benchmark/data/samples/retrieval_docid_sample.json
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/download.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/prepare_external_datasets.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/registry.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/base.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/judge_utils.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/llm_judge.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/mock_judge.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/prompts.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/answer_correctness.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/coverage.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/exact_match.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/faithfulness.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/rouge_l.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/answer/token_f1.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/base.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/conflict_detection.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/entity_f1.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/graph_structure.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/property_f1.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/schema_validity.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/structural_integrity.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/syntax_validity.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/temporal_validity.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/triple_f1.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/registry.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/context_precision.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/context_relevancy.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/evidence_recall.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/hit_at_k.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/mrr.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/retrieval/recall_at_k.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/models/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/models/result.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/reporters/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/reporters/json_reporter.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/reporters/markdown_reporter.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/runners/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/runners/ablation_runner.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/runners/answer_runner.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/runners/base_runner.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/runners/extraction_runner.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/runners/retrieval_runner.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/utils/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/utils/graph_extract.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/utils/normalize.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/utils/retrieval_adapter.py
  • hugegraph-llm/src/hugegraph_llm/config/llm_config.py
  • hugegraph-llm/src/hugegraph_llm/flows/graph_extract.py
  • hugegraph-llm/src/hugegraph_llm/models/embeddings/openai.py
  • hugegraph-llm/src/hugegraph_llm/models/llms/openai.py
  • hugegraph-llm/src/hugegraph_llm/models/rerankers/init_reranker.py
  • hugegraph-llm/src/hugegraph_llm/models/rerankers/jina.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/info_extract.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/schema_build.py
  • hugegraph-llm/src/hugegraph_llm/state/ai_state.py
  • hugegraph-llm/src/hugegraph_llm/utils/embedding_utils.py
  • hugegraph-llm/src/tests/benchmark/__init__.py
  • hugegraph-llm/src/tests/benchmark/test_answer_metrics.py
  • hugegraph-llm/src/tests/benchmark/test_base_runner.py
  • hugegraph-llm/src/tests/benchmark/test_baseline.py
  • hugegraph-llm/src/tests/benchmark/test_cli.py
  • hugegraph-llm/src/tests/benchmark/test_conflict_detection.py
  • hugegraph-llm/src/tests/benchmark/test_e2e_car_dataset.py
  • hugegraph-llm/src/tests/benchmark/test_e2e_cli.py
  • hugegraph-llm/src/tests/benchmark/test_extraction_metrics.py
  • hugegraph-llm/src/tests/benchmark/test_graph_extract.py
  • hugegraph-llm/src/tests/benchmark/test_graph_structure.py
  • hugegraph-llm/src/tests/benchmark/test_integration_ablation.py
  • hugegraph-llm/src/tests/benchmark/test_integration_extraction.py
  • hugegraph-llm/src/tests/benchmark/test_integration_retrieval.py
  • hugegraph-llm/src/tests/benchmark/test_json_parse_utils.py
  • hugegraph-llm/src/tests/benchmark/test_llm_judge_metrics.py
  • hugegraph-llm/src/tests/benchmark/test_markdown_reporter.py
  • hugegraph-llm/src/tests/benchmark/test_prepare_external_datasets.py
  • hugegraph-llm/src/tests/benchmark/test_registry_fix.py
  • hugegraph-llm/src/tests/benchmark/test_reproducibility.py
  • hugegraph-llm/src/tests/benchmark/test_retrieval_adapter.py
  • hugegraph-llm/src/tests/benchmark/test_retrieval_metrics.py
  • hugegraph-llm/src/tests/benchmark/test_retrieval_runner.py
  • hugegraph-llm/src/tests/benchmark/test_temporal_validity.py
  • hugegraph-llm/src/tests/document/test_graph_extract_configurable_split.py

Comment on lines +1 to +13
#!/usr/bin/env python3
"""Fix edge endpoint IDs in car33 pipeline candidate JSONs.

HugeGraph-AI GRAPH_EXTRACT outputs edges with ``outV``/``inV`` values like
``"1:自动远光灯开启指示灯"``, while vertices use the clean ``name`` field
(``"自动远光灯开启指示灯"``). This mismatch causes the benchmark to treat
all edges as orphan edges.

This script reads an existing candidate JSON (which already contains the
raw LLM outputs) and rewrites the edge endpoints by stripping the ``<id>:``
prefix. The fixed JSON can then be fed back into ``hugegraph-benchmark run``
without re-running the expensive LLM extraction.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

缺少 ASF License Header,CI 已报错。

Pipeline 报告 license-eye header check failed,该文件目前没有标准的 Apache License Header。

📄 建议在文件顶部添加
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 #!/usr/bin/env python3
 """Fix edge endpoint IDs in car33 pipeline candidate JSONs.
🧰 Tools
🪛 GitHub Actions: License header & 3rd-party check / 1_check-license-header.txt

[error] 1-1: license-eye header check failed: file does not have a valid license header.

🪛 GitHub Actions: License header & 3rd-party check / check-license-header

[error] 1-1: license-eye: file does not have a valid license header

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/fix_car33_edge_ids.py` around lines 1 - 13,
The script file is missing the required Apache License header, causing the CI
license-eye header check to fail. Add the standard ASF license block at the very
top of the file before the module docstring, keeping the existing script content
unchanged; use this file’s top-level entry point and docstring as the location
to insert the header.

Source: Pipeline failures

Comment on lines +49 to +52
REPO_ROOT = Path(__file__).resolve().parents[3]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files 'hugegraph-llm/scripts/benchmark/*' && printf '\n---\n' && sed -n '1,120p' hugegraph-llm/scripts/benchmark/generate_hugegraph_retrieval_outputs.py && printf '\n---\n' && sed -n '1,120p' hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.py

Repository: hugegraph/hugegraph-ai

Length of output: 10169


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

p = Path('hugegraph-llm/scripts/benchmark/generate_hugegraph_retrieval_outputs.py').resolve()
print("resolved file:", p)
for i, parent in enumerate(p.parents[:6]):
    print(i, parent)
PY

Repository: hugegraph/hugegraph-ai

Length of output: 443


REPO_ROOT 层级少一层

Path(__file__).resolve().parents[3] 会越过 hugegraph-llm,把 SRC_ROOT 指到错误的 src;直接运行脚本且未预装依赖时会导入失败。这里应改成 parents[2]

🐛 建议修复
-REPO_ROOT = Path(__file__).resolve().parents[3]
+REPO_ROOT = Path(__file__).resolve().parents[2]
 SRC_ROOT = REPO_ROOT / "src"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REPO_ROOT = Path(__file__).resolve().parents[3]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))
REPO_ROOT = Path(__file__).resolve().parents[2]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/generate_hugegraph_retrieval_outputs.py`
around lines 49 - 52, REPO_ROOT is computed one level too high in
generate_hugegraph_retrieval_outputs.py, so SRC_ROOT can point at the wrong src
directory and break imports when the script runs directly. Update the REPO_ROOT
assignment in the script bootstrap section to use the correct
Path(__file__).resolve().parents value, and keep the existing sys.path insertion
logic tied to SRC_ROOT so the script resolves local dependencies properly.

Comment thread hugegraph-llm/scripts/benchmark/generate_hugegraph_retrieval_outputs.py Outdated
Comment on lines +539 to +552
pipeline = manager.fetch()
if pipeline is None:
pipeline = flow.build_flow(query=query, rerank_method="bleu", topk_return_results=topk)
status = pipeline.init()
if status.isErr():
raise RuntimeError(f"rag_graph_vector init failed: {status.getInfo()}")
status = pipeline.run()
if status.isErr():
manager.add(pipeline)
raise RuntimeError(f"rag_graph_vector run failed: {status.getInfo()}")
state = pipeline.getGParamWithNoEmpty("wkflow_state").to_json()
result = flow.post_deal(pipeline)
manager.add(pipeline)
return {"state": state, "result": result}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

pipeline.init() 失败分支未将 pipeline 归还 manager,与其余分支处理不一致。

第 546-548 行 run() 失败时会先 manager.add(pipeline) 再抛出异常,但第 542-544 行 init() 失败时直接 raise,未调用 manager.add(pipeline)。若 init() 已经分配了底层资源(连接、GParam 注册等),该 pipeline 实例会被直接丢弃且不再进入池中管理,造成资源未被追踪/复用不一致。

🔧 建议修复
         status = pipeline.init()
         if status.isErr():
+            manager.add(pipeline)
             raise RuntimeError(f"rag_graph_vector init failed: {status.getInfo()}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pipeline = manager.fetch()
if pipeline is None:
pipeline = flow.build_flow(query=query, rerank_method="bleu", topk_return_results=topk)
status = pipeline.init()
if status.isErr():
raise RuntimeError(f"rag_graph_vector init failed: {status.getInfo()}")
status = pipeline.run()
if status.isErr():
manager.add(pipeline)
raise RuntimeError(f"rag_graph_vector run failed: {status.getInfo()}")
state = pipeline.getGParamWithNoEmpty("wkflow_state").to_json()
result = flow.post_deal(pipeline)
manager.add(pipeline)
return {"state": state, "result": result}
pipeline = manager.fetch()
if pipeline is None:
pipeline = flow.build_flow(query=query, rerank_method="bleu", topk_return_results=topk)
status = pipeline.init()
if status.isErr():
manager.add(pipeline)
raise RuntimeError(f"rag_graph_vector init failed: {status.getInfo()}")
status = pipeline.run()
if status.isErr():
manager.add(pipeline)
raise RuntimeError(f"rag_graph_vector run failed: {status.getInfo()}")
state = pipeline.getGParamWithNoEmpty("wkflow_state").to_json()
result = flow.post_deal(pipeline)
manager.add(pipeline)
return {"state": state, "result": result}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/generate_hugegraph_retrieval_outputs.py`
around lines 539 - 552, The init failure path in fetch/build_flow handling is
inconsistent with the run failure path because pipeline.init() raises without
returning the created pipeline to manager. In the flow that creates the
pipeline, add manager.add(pipeline) before raising on an init error so the same
resource management behavior is used as in the pipeline.run() failure branch;
use the pipeline, manager.add, init, and run symbols to update the error
handling consistently.

Comment on lines +39 to +42
REPO_ROOT = Path(__file__).resolve().parents[3]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 核实脚本相对 hugegraph_llm 包的实际目录深度
fd -t d hugegraph_llm hugegraph-llm/src
fd fix_car33_edge_ids.py generate_text2kgbench_candidates.py

Repository: hugegraph/hugegraph-ai

Length of output: 316


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 定位目标文件并查看相关行
git ls-files 'hugegraph-llm/scripts/benchmark/*candidates.py' 'hugegraph-llm/scripts/benchmark/*outputs.py'
echo '---'
for f in $(git ls-files 'hugegraph-llm/scripts/benchmark/*candidates.py' 'hugegraph-llm/scripts/benchmark/*outputs.py'); do
  echo "FILE: $f"
  wc -l "$f"
  echo '--- head/tail ---'
  sed -n '1,120p' "$f" | nl -ba | sed -n '1,120p'
  echo '===='
done

# 用只读 Python 计算目标脚本的 parents 层级
python3 - <<'PY'
from pathlib import Path
p = Path('hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.py').resolve()
print('resolved:', p)
for i in range(6):
    print(i, p.parents[i])
PY

Repository: hugegraph/hugegraph-ai

Length of output: 517


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 精确搜索 REPO_ROOT / SRC_ROOT 片段,以及另一个脚本是否同样计算
rg -n "Path\\(__file__\\)\\.resolve\\(\\)\\.parents\\[[0-9]+\\]|SRC_ROOT = REPO_ROOT / \"src\"|sys\\.path\\.insert" hugegraph-llm/scripts/benchmark

Repository: hugegraph/hugegraph-ai

Length of output: 1862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.py').resolve()
print('resolved file:', p)
for i in range(5):
    parent = p.parents[i]
    print(f'parents[{i}]: {parent}  exists={parent.exists()}')
print('repo_root_candidate (parents[3]):', p.parents[3], 'src_exists=', (p.parents[3] / 'src').exists())
print('hugegraph_llm_src_exists=', (p.parents[2] / 'src').exists())
print('hugegraph_llm_package_exists=', (p.parents[2] / 'src' / 'hugegraph_llm').exists())
PY

echo '---'
git ls-files 'src/**' 'hugegraph-llm/src/**' | sed -n '1,80p'

Repository: hugegraph/hugegraph-ai

Length of output: 6049


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import importlib.util, sys
from pathlib import Path

script = Path('hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.py').resolve()
repo_root = script.parents[3]
llm_root = script.parents[2]
wrong_src = repo_root / 'src'
right_src = llm_root / 'src'

def probe(label, paths):
    old = sys.path[:]
    sys.path[:] = [str(p) for p in paths] + old
    spec = importlib.util.find_spec('hugegraph_llm')
    print(label, 'paths=', [str(p) for p in paths], 'spec_origin=', None if spec is None else spec.origin)
    sys.path[:] = old

print('repo_root=', repo_root)
print('wrong_src_exists=', wrong_src.exists())
print('right_src_exists=', right_src.exists())
print('pkg_exists=', (right_src / 'hugegraph_llm').exists())
probe('wrong_only', [wrong_src])
probe('right_only', [right_src])
probe('both', [wrong_src, right_src])
PY

Repository: hugegraph/hugegraph-ai

Length of output: 607


REPO_ROOT 多算了一层,直跑脚本时会找不到 hugegraph_llm

hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.pygenerate_hugegraph_retrieval_outputs.py 里都用了 Path(__file__).resolve().parents[3],这会把 REPO_ROOT 指到仓库上一级目录,随后 SRC_ROOT = REPO_ROOT / "src" 变成不存在的路径。未安装包时直接运行脚本会因此 import hugegraph_llm 失败;这里应改成 parents[2]

建议修复
-REPO_ROOT = Path(__file__).resolve().parents[3]
+REPO_ROOT = Path(__file__).resolve().parents[2]
 SRC_ROOT = REPO_ROOT / "src"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REPO_ROOT = Path(__file__).resolve().parents[3]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))
REPO_ROOT = Path(__file__).resolve().parents[2]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/generate_text2kgbench_candidates.py` around
lines 39 - 42, `REPO_ROOT` in the benchmark script is computed one level too
high, which makes `SRC_ROOT` point outside the repository and breaks direct
imports of `hugegraph_llm` when running the script standalone. Update the path
setup in `generate_text2kgbench_candidates.py` (and the matching logic in
`generate_hugegraph_retrieval_outputs.py`) to use the correct ancestor from
`__file__`, so `REPO_ROOT` resolves to the repository root and `SRC_ROOT`
remains the actual `src` directory used for `sys.path` insertion.

Comment on lines +79 to +104
def _run_metric_safe(
self,
metric: BaseMetric,
prediction: Any,
reference: Any,
sample_id: str,
**kwargs: Any,
) -> Dict[str, float]:
"""Run a metric with error tracking (thread-safe).

On success returns the metric scores dict.
On failure records the error in ``self._errors`` and returns ``{}``.
"""
try:
return metric.calculate(prediction=prediction, reference=reference, **kwargs)
except Exception as e:
with self._errors_lock:
self._errors.append(
{
"sample_id": sample_id,
"metric": metric.name,
"error": str(e),
}
)
logger.exception("Metric %s failed for sample %s", metric.name, sample_id)
return {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

建议在基类中统一校验 metric.requires_llm,而不是让各 runner 自行实现。

BaseMetric 已经有 requires_llm 属性(见 test_base_runner.py 中的 stub 指标)。目前只有 RetrievalRunner 手工维护了一份 _CONTEXT_METRICS 集合来做 fail-fast 校验(llm is None 时提前报错),而 AnswerRunner 完全没有类似校验,依赖需要 LLM 的指标(如 answer_correctnessfaithfulness)在缺少 llm 时静默失败并进入 _errors

建议在 _create_metric_instances_run_metric_safe 中基于 metric.requires_llm 统一做前置校验,这样所有 runner(包括未来新增的)都能获得一致的 fail-fast 行为,减少每个 runner 重复实现校验逻辑的成本。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/runners/base_runner.py` around
lines 79 - 104, BaseRunner currently runs metrics without checking whether they
require an LLM, so metrics like answer_correctness and faithfulness can fail
silently instead of failing fast. Add a centralized pre-check in BaseRunner,
ideally in _create_metric_instances or _run_metric_safe, that inspects each
BaseMetric.requires_llm and raises/short-circuits when llm is missing; then
remove runner-specific duplication like RetrievalRunner’s _CONTEXT_METRICS so
all runners get consistent behavior.

Comment thread hugegraph-llm/src/hugegraph_llm/benchmark/utils/graph_extract.py
Comment on lines 50 to 53
def get_text_embedding(self, text: str) -> List[float]:
"""Comment"""
response = self.client.embeddings.create(input=text, model=self.model)
"""Get embedding for a single text with retry."""
response = self._embed_with_retry([text])
return response.data[0].embedding

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

单文本 embedding 路径缺少截断/重试保护,与批量路径不一致

批量方法 (get_texts_embeddings / async_get_texts_embeddings) 都先调用 _truncate_texts 再走 _embed_with_retry / _async_embed_with_retry,但:

  • get_text_embedding(第 50-53 行)只做了重试,没有截断,超长单文本仍可能超出 provider 的 token 上限(正是引入截断要解决的问题)。
  • async_get_text_embedding(第 97-99 行)既没有截断也没有重试,直接调用 self.aclient.embeddings.create,是本类中唯一没有瞬时错误重试保护的外部调用路径。

建议让这两个方法与批量路径保持一致的截断+重试策略。

🛡️ 建议的修改
     def get_text_embedding(self, text: str) -> List[float]:
         """Get embedding for a single text with retry."""
-        response = self._embed_with_retry([text])
+        response = self._embed_with_retry(self._truncate_texts([text]))
         return response.data[0].embedding
     async def async_get_text_embedding(self, text: str) -> List[float]:
-        response = await self.aclient.embeddings.create(input=[text], model=self.model)
+        response = await self._async_embed_with_retry(self._truncate_texts([text]))
         return response.data[0].embedding

Also applies to: 97-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/models/embeddings/openai.py` around lines 50
- 53, The single-text embedding paths are inconsistent with the batch APIs:
get_text_embedding and async_get_text_embedding skip the truncation and retry
protections used by get_texts_embeddings and async_get_texts_embeddings. Update
these methods in openai.py to first pass input through the same _truncate_texts
flow as the batch methods, then use _embed_with_retry and
_async_embed_with_retry respectively, so both sync and async single-text calls
match the provider safety behavior.

Comment on lines +1 to +16
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

许可证头缺少 "the",导致 CI 许可证检查失败。

第 5 行为 # to you under Apache License, Version 2.0 (the,缺少 "the"(应为 to you under the Apache License, Version 2.0 (the),与本文件其他两处失败的 license-eye 检查完全吻合。

🐛 建议修复
-# to you under Apache License, Version 2.0 (the
+# to you under the Apache License, Version 2.0 (the
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
🧰 Tools
🪛 GitHub Actions: License header & 3rd-party check / 1_check-license-header.txt

[error] 1-1: license-eye header check failed: file does not have a valid license header.

🪛 GitHub Actions: License header & 3rd-party check / check-license-header

[error] 1-1: license-eye: file does not have a valid license header

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/models/rerankers/jina.py` around lines 1 -
16, License header text is missing “the” in the Apache 2.0 notice, causing the
license check to fail. Update the header in jina.py so the standard ASF license
line matches the expected wording used elsewhere in the repo, keeping the same
header block and correcting the phrase in the license sentence.

Source: Pipeline failures

Comment thread hugegraph-llm/src/hugegraph_llm/operators/llm_op/schema_build.py Outdated
Postroggy added 8 commits July 5, 2026 19:48
…dules to origin/main

- Delete docs/quality/benchmark-code-style-spec.md, hugegraph-llm/BENCHMARK_DATASETS.md,
  hugegraph-llm/GRAPHRAG_BENCHMARK.md and hugegraph-llm/docs/benchmark/experiment-*.md.
- Delete the entire hugegraph-llm/scripts/benchmark/ directory.
- Revert local changes to llm_config.py, graph_extract.py, embeddings/llms openai.py,
  rerankers, info_extract.py, property_graph_extract.py, schema_build.py,
  ai_state.py, embedding_utils.py and test_graph_extract_configurable_split.py.
- Keep benchmark-only changes: MetricRegistry direction arrows in MarkdownReporter,
  removal of the misleading Low-performing Samples section, and ruff formatting.
- Fix benchmark CLI to only import project llm_settings when no explicit settings
  are provided, avoiding local .env reranker_type=jina failures in tests.
…other modules to origin/main"

This reverts commit 209c388.
…y scope

This re-applies the cleanup from 209c388:
- Delete docs/quality/benchmark-code-style-spec.md
- Delete hugegraph-llm/BENCHMARK_DATASETS.md and GRAPHRAG_BENCHMARK.md
- Delete hugegraph-llm/docs/benchmark/experiment-*.md
- Delete the entire hugegraph-llm/scripts/benchmark/ directory
- Revert local changes to llm_config.py, graph_extract.py, embeddings/llms openai.py,
  rerankers, info_extract.py, property_graph_extract.py, schema_build.py,
  ai_state.py, embedding_utils.py and test_graph_extract_configurable_split.py
Rewrite MarkdownReporter into a four-layer inverted-pyramid layout
designed for PR/Issue comments: 概览 (TL;DR) → 分析 (programmatic
roll-up) → 指标总览 (changed-only) → 退化/改进样例 (one row per
sample, detail folded) → 证据层 (folded).

- Add metrics/dimensions.py: metric → (domain, sub-dimension) mapping,
  the single source of truth replacing the old _METRIC_GROUPS table.
  Presentation-only; does NOT influence regression verdicts.
- ComparisonResult: add baseline_overall/candidate_overall (true
  before/after values, no reverse-engineering from delta), keep
  analyze() as a pure function with no memoization hacks.
- analyze(): roll up by domain/sub-dimension, detect question-type
  clustering and per-sample concentration; add DEFAULT_RATIO_DELTA
  (0.01) so sub-1% wobble is treated as 持平 instead of 退化/改进 noise.
- Per-sample regression judgment also floors at DEFAULT_RATIO_DELTA,
  keeping LLM-Judge's higher 0.05 tolerance.
读一份真实 baseline,按 metric 维度分桶注入可控扰动(recall@1 退化、
mrr/hit@5 改进、其余抖动),生成一个有真实感的 candidate,让 compare
能现场演示退化/改进/持平的全谱,无需重跑评测流程。用于演示测评闭环。

可复现(固定 --seed),幅度由 --magnitude 控制。
…ehavior

- download.py: support HF parquet format for hotpotqa/2wikimultihopqa/musique
- prepare_external_datasets.py: add sentence-level gold_evidence extraction
- registry.py: update dataset postprocessing configs
- syntax_validity.py: raise ValueError on empty raw_responses instead of silent 0
- .gitignore: add AGENTS.local.md
…SemanticTripleF1, ExtractionFaithfulness)

- semantic_entity_f1.py: LLM judge matches candidate↔gold entities via semantic equivalence
- semantic_triple_f1.py: LLM judge matches candidate↔gold triples (source/relation/target/direction)
- extraction_faithfulness.py: GT-free faithfulness check against input text (deepeval NLI pattern)
- prompts.py: add 3 prompt templates × EN/ZH with few-shot examples (car33 评分规则 aligned)
- extraction_runner.py: register new metric data mappings + pass input_text for faithfulness
- dimensions.py: classify semantic metrics under 'extraction → 语义匹配' sub-dimension

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (1)
hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/__init__.py (1)

40-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

循环导入设计脆弱,建议抽取公共辅助模块

_edge_in/_edge_out 定义在本文件内,而 semantic_triple_f1.pyextraction_faithfulness.py 又反向从本包 __init__ 导入它们(from hugegraph_llm.benchmark.metrics.extraction import _edge_in, _edge_out)。当前能正常工作纯粹依赖 __init__.py 内部“先定义辅助函数、再导入子模块”的顺序;一旦顺序被打乱(如新增子模块导入插在辅助函数定义之前),将触发 ImportError

建议将 _is_edge/_edge_out/_edge_in 抽取到独立模块(如 _edge_utils.py),由 __init__.py 和各 metric 文件平等导入,消除对包初始化顺序的隐性依赖。

♻️ 建议方案
+# hugegraph_llm/benchmark/metrics/extraction/_edge_utils.py
+def _is_edge(item): ...
+def _edge_out(item): ...
+def _edge_in(item): ...
-# extraction/__init__.py 中的 _is_edge/_edge_out/_edge_in 定义
+from hugegraph_llm.benchmark.metrics.extraction._edge_utils import _is_edge, _edge_out, _edge_in
-from hugegraph_llm.benchmark.metrics.extraction import _edge_in, _edge_out
+from hugegraph_llm.benchmark.metrics.extraction._edge_utils import _edge_in, _edge_out
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/__init__.py`
around lines 40 - 50, 当前包初始化依赖顺序脆弱:`_edge_in`/_edge_out`/`_is_edge` 定义在
`hugegraph_llm.benchmark.metrics.extraction.__init__`,而
`semantic_triple_f1.py`、`extraction_faithfulness.py`
又从包入口反向导入这些符号。请把这些公共辅助函数抽到独立模块(例如 `_edge_utils.py`),然后让
`__init__`、`semantic_triple_f1`、`extraction_faithfulness` 统一从该模块导入,避免对
`__init__` 内部定义顺序的隐式依赖。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hugegraph-llm/scripts/benchmark/jitter_baseline.py`:
- Around line 166-168: The Ruff format check is failing in the
jitter_baseline.py benchmark script, so reformat the affected section and any
related surrounding code with the project formatter. Use the existing
benchmark/reporting code around the degradation/improvement printing in the main
comparison flow, then run uv run ruff format . to ensure the file passes ruff
format --check . before resubmitting.
- Around line 65-66: The prefix check in jitter_baseline.py is too broad and
causes recall@10/recall@100 to match recall@1 in the regression bucket logic.
Tighten the matching in the metric classification path inside the function that
returns "down" so it only treats exact top-1 metrics as recall@1, or uses a
delimiter-aware prefix match, while keeping the existing REGRESS_PREFIXES
handling for other metrics.
- Around line 81-94: In the baseline jittering logic that iterates over metrics
in jitter_baseline.py, avoid applying the [0, 1] clamp to
count-like/sample-level metrics such as semantic_entity_matched. Update the
metric mutation path so only normalized ratio metrics are clamped, while integer
count metrics keep their natural non-negative scale after applying noise. Use
the existing direction/_direction_for flow to distinguish count metrics and
preserve valid candidate baseline values for compare reports.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/datasets/download.py`:
- Around line 204-207: The context_list construction in the download dataset
flow is treating string sentences as generic iterables, so list(s) splits them
into characters. Update the logic in the loop/comprehension around context_list
to special-case str before the iterable branch, and only convert non-string
iterables to lists while wrapping plain strings as a single-item list.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/prompts.py`:
- Around line 675-684: The matching rules in prompts.py are inconsistent about
the warning-light exception: the general rule requires exact label equality, but
the special case then permits cross-type matching for Component/Status. Update
the wording around the matching rules section to explicitly state whether this
exception is allowed, and if so, list the exact label combinations it applies
to; also make the example consistent with that rule so the LLM judge behavior is
unambiguous.
- Around line 800-813: The few-shot example in the prompt contains a typo in the
relation name, so update the example inside the prompt template in prompts.py to
use the same relation symbol as the candidate triple. Fix the mismatch in the
example’s reasoning text and keep the relation name consistent with the
HAS_COMPONENT triple referenced in the example.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/extraction_faithfulness.py`:
- Line 1: The Python formatting check is failing for the extraction_faithfulness
module, so reformat the codebase with the project’s Ruff formatter and ensure it
matches the existing style. Apply the formatting fix to the touched Python code
in extraction_faithfulness.py, then rerun the required root checks with ruff
format and ruff check before handing off.
- Around line 89-100: The extraction_faithfulness metric silently returns all
None when prediction is neither dict nor list, making upstream data-format
errors indistinguishable from the no-LLM path. In the extraction_faithfulness
logic, add a warning/error log in the invalid-prediction branch before returning
the None result, and include the unexpected prediction type or a safe summary of
its value so callers can trace malformed input while keeping the existing return
shape unchanged.
- Around line 125-146: The extraction faithfulness counting in the judgment
aggregation is using raw verdict entries, which can overcount duplicated or
extra positive items. Update the logic in extraction_faithfulness.py to read the
returned verdicts by idx, ignore out-of-range indices, and deduplicate before
counting faithful items so the total stays consistent with items. Also make sure
the parsing/aggregation around retry_llm_call, _parse_json_response, and the
verdicts loop uses idx (not index) from the model output.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/semantic_entity_f1.py`:
- Line 122: The current change in semantic_entity_f1.py needs Ruff formatting
cleanup because ruff format --check is failing. Run uv run ruff format . from
the repository root and ensure the formatted result is applied to the
semantic_entity_f1.py logic around the F1 calculation so the file matches Ruff’s
expected style before committing.
- Around line 108-118: The `SemanticEntityF1` match parsing currently trusts
`data["matches"]` too much, which can inflate `matched` when the LLM returns
duplicate, out-of-range, or non-integer indices. Update the match normalization
logic in `semantic_entity_f1.py` to validate each pair against `gold_lines` and
`cand_lines`, discard invalid indices, and deduplicate matches before computing
`matched`, `gold_count`, and `cand_count`. Keep the fix localized to the match
extraction block so `matches` only contains unique, in-bounds integer pairs
before the metric calculation.
- Around line 52-58: 实体行当前是在 semantic_entity_f1.py 的格式化逻辑里手写拼接 JSON-like 字符串,未对
label/name 做转义,容易在包含引号、换行或花括号时破坏 prompt 结构;请在该段遍历 vertices 的代码中改为使用 json.dumps
生成每一行的实体文本,确保 label 和 name 都按 JSON 规则序列化后再 append 到 lines,从而避免特殊字符污染输出。

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/semantic_triple_f1.py`:
- Line 1: CI formatting check failed because the Python file is not
Ruff-formatted. Run the repository root formatting command to apply the required
style, then verify the change in semantic_triple_f1.py and any related Python
edits with the same Ruff workflow before handing off. Use the file’s module
content in hugegraph_llm.benchmark.metrics.extraction.semantic_triple_f1 and
ensure the final diff passes the formatting check.
- Around line 75-81: The empty-triple case in the semantic metric is being
treated as a worst-case miss, but `semantic_triple_f1` should consider
`prediction` and `reference` both empty as a perfect match. Update the
empty-input branch in `semantic_triple_f1` so it returns precision/recall/f1 as
1.0 (with matched count 0 or equivalent) when both lists are empty, while
keeping the existing handling for one-sided empties unchanged. Use the
`semantic_triple_f1` function as the anchor when adjusting this logic.
- Around line 99-124: In the semantic triple F1 metric logic, the current
handling of LLM-returned matches only checks that each entry is a 2-item list,
so duplicate pairs or out-of-range indices can inflate matched counts and make
precision/recall invalid. Update the match normalization in the code path that
uses retry_llm_call and _parse_json_response to validate each (gold_idx,
cand_idx) against gold_lines and cand_lines bounds, deduplicate valid pairs
before counting, and compute matched from the filtered unique set so the
returned semantic_triple_precision, semantic_triple_recall, and
semantic_triple_f1 stay bounded and comparable.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/reporters/markdown_reporter.py`:
- Around line 289-317: In `_section_samples`, the sorting direction for
summarized rows is wrong for the “improvements” case:
`summarized.sort(key=lambda r: r["worst_delta"])` works for regressions but
causes the largest improvements to fall out of the top-N. Update the sort logic
in `_section_samples` (using `change_key`, `summarized`, and `worst_delta`) so
regressions stay ascending by most negative delta, while improvements are
ordered to show the largest positive deltas first before applying `limit`.

---

Nitpick comments:
In `@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/__init__.py`:
- Around line 40-50: 当前包初始化依赖顺序脆弱:`_edge_in`/_edge_out`/`_is_edge` 定义在
`hugegraph_llm.benchmark.metrics.extraction.__init__`,而
`semantic_triple_f1.py`、`extraction_faithfulness.py`
又从包入口反向导入这些符号。请把这些公共辅助函数抽到独立模块(例如 `_edge_utils.py`),然后让
`__init__`、`semantic_triple_f1`、`extraction_faithfulness` 统一从该模块导入,避免对
`__init__` 内部定义顺序的隐式依赖。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9493a029-1242-413f-acf8-18b265d7c491

📥 Commits

Reviewing files that changed from the base of the PR and between bb750d0 and 9eb31fd.

📒 Files selected for processing (15)
  • .gitignore
  • hugegraph-llm/scripts/benchmark/jitter_baseline.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/baseline/compare.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/download.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/prepare_external_datasets.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/registry.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/prompts.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/dimensions.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/__init__.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/extraction_faithfulness.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/semantic_entity_f1.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/semantic_triple_f1.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/syntax_validity.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/reporters/markdown_reporter.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/runners/extraction_runner.py
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (4)
  • hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/syntax_validity.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/runners/extraction_runner.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/registry.py
  • hugegraph-llm/src/hugegraph_llm/benchmark/datasets/prepare_external_datasets.py

Comment on lines +65 to +66
if any(metric == p or metric.startswith(p) for p in REGRESS_PREFIXES):
return "down"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

避免把 recall@10 误判为 recall@1

Line 65 的 startswith("recall@1") 会同时匹配 recall@10recall@100,导致非 top-1 指标被错误标记为退化 bucket。

建议修复
-    if any(metric == p or metric.startswith(p) for p in REGRESS_PREFIXES):
+    if metric in REGRESS_PREFIXES:
         return "down"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if any(metric == p or metric.startswith(p) for p in REGRESS_PREFIXES):
return "down"
if metric in REGRESS_PREFIXES:
return "down"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/jitter_baseline.py` around lines 65 - 66, The
prefix check in jitter_baseline.py is too broad and causes recall@10/recall@100
to match recall@1 in the regression bucket logic. Tighten the matching in the
metric classification path inside the function that returns "down" so it only
treats exact top-1 metrics as recall@1, or uses a delimiter-aware prefix match,
while keeping the existing REGRESS_PREFIXES handling for other metrics.

Comment on lines +81 to +94
for name, value in list(metrics.items()):
if value is None or not isinstance(value, (int, float)):
continue
direction = _direction_for(name)
if direction == "down":
# 退化:大概率显著下降
delta = -magnitude * (0.5 + rng.random())
elif direction == "up":
# 改进:大概率上升
delta = magnitude * (0.5 + rng.random())
else:
# 噪音:小双向
delta = (rng.random() - 0.5) * magnitude * 0.4
metrics[name] = round(_clamp01(value + delta), 4)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

不要对计数型指标做 [0, 1] 裁剪。

semantic_entity_matched 这类 sample-level 计数会被当前逻辑抖动并裁剪成 0~1,生成的 candidate baseline 会包含不可能的计数值,影响 compare 报告可信度。

建议修复
+COUNT_METRIC_SUFFIXES = ("_matched", "_count")
+
...
         for name, value in list(metrics.items()):
             if value is None or not isinstance(value, (int, float)):
                 continue
+            if name.endswith(COUNT_METRIC_SUFFIXES):
+                continue
             direction = _direction_for(name)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for name, value in list(metrics.items()):
if value is None or not isinstance(value, (int, float)):
continue
direction = _direction_for(name)
if direction == "down":
# 退化:大概率显著下降
delta = -magnitude * (0.5 + rng.random())
elif direction == "up":
# 改进:大概率上升
delta = magnitude * (0.5 + rng.random())
else:
# 噪音:小双向
delta = (rng.random() - 0.5) * magnitude * 0.4
metrics[name] = round(_clamp01(value + delta), 4)
COUNT_METRIC_SUFFIXES = ("_matched", "_count")
for name, value in list(metrics.items()):
if value is None or not isinstance(value, (int, float)):
continue
if name.endswith(COUNT_METRIC_SUFFIXES):
continue
direction = _direction_for(name)
if direction == "down":
# 退化:大概率显著下降
delta = -magnitude * (0.5 + rng.random())
elif direction == "up":
# 改进:大概率上升
delta = magnitude * (0.5 + rng.random())
else:
# 噪音:小双向
delta = (rng.random() - 0.5) * magnitude * 0.4
metrics[name] = round(_clamp01(value + delta), 4)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/jitter_baseline.py` around lines 81 - 94, In
the baseline jittering logic that iterates over metrics in jitter_baseline.py,
avoid applying the [0, 1] clamp to count-like/sample-level metrics such as
semantic_entity_matched. Update the metric mutation path so only normalized
ratio metrics are clamped, while integer count metrics keep their natural
non-negative scale after applying noise. Use the existing
direction/_direction_for flow to distinguish count metrics and preserve valid
candidate baseline values for compare reports.

Comment on lines +166 to +168
print(f" 退化 {k}: {orig_overall[k]:.4f} -> {new_overall[k]:.4f} ({d:+.4f})", file=sys.stderr)
for k, d in sorted(improved, key=lambda x: -x[1])[:5]:
print(f" 改进 {k}: {orig_overall[k]:.4f} -> {new_overall[k]:.4f} ({d:+.4f})", file=sys.stderr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

先修复 Ruff 格式化失败。

CI 已报告该文件 ruff format --check . 不通过;提交前请运行 uv run ruff format .

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/scripts/benchmark/jitter_baseline.py` around lines 166 - 168,
The Ruff format check is failing in the jitter_baseline.py benchmark script, so
reformat the affected section and any related surrounding code with the project
formatter. Use the existing benchmark/reporting code around the
degradation/improvement printing in the main comparison flow, then run uv run
ruff format . to ensure the file passes ruff format --check . before
resubmitting.

Source: Pipeline failures

Comment on lines +204 to +207
context_list = [
[str(t), list(s) if hasattr(s, "__iter__") else [str(s)]]
for t, s in zip(titles, sentences)
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

避免把单句字符串拆成字符。

sentences 的元素已经是字符串时,list(s) 会变成字符数组,写出的 corpus/QA 会被破坏;需要先单独处理 str

建议修复
-            context_list = [
-                [str(t), list(s) if hasattr(s, "__iter__") else [str(s)]]
-                for t, s in zip(titles, sentences)
-            ]
+            context_list = []
+            for t, s in zip(titles, sentences):
+                if isinstance(s, str):
+                    sentence_list = [s]
+                elif hasattr(s, "__iter__"):
+                    sentence_list = [str(item) for item in s]
+                else:
+                    sentence_list = [str(s)]
+                context_list.append([str(t), sentence_list])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
context_list = [
[str(t), list(s) if hasattr(s, "__iter__") else [str(s)]]
for t, s in zip(titles, sentences)
]
context_list = []
for t, s in zip(titles, sentences):
if isinstance(s, str):
sentence_list = [s]
elif hasattr(s, "__iter__"):
sentence_list = [str(item) for item in s]
else:
sentence_list = [str(s)]
context_list.append([str(t), sentence_list])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/datasets/download.py` around lines
204 - 207, The context_list construction in the download dataset flow is
treating string sentences as generic iterables, so list(s) splits them into
characters. Update the logic in the loop/comprehension around context_list to
special-case str before the iterable branch, and only convert non-string
iterables to lists while wrapping plain strings as a single-item list.

Comment on lines +675 to +684
Matching rules:
- Entity TYPE (label) must match exactly. Component ≠ Function, Status ≠ Specification.
- Entity NAME allows: synonym normalization, abbreviation expansion, phrasing variation.
Example: "制动液" matches "制动液检查/更换" (same core concept, different granularity).
- Each gold entity can be matched at most once.
- Each candidate entity can be matched at most once.
- If two candidate entities match the same gold entity, the first one wins.
- Special case: A warning-light Component in the gold that is expressed as
Status in the candidate may still match if the semantic signal is identical
(e.g., gold "Status(ABS故障警告灯)" ↔ candidate "Status(ABS系统故障指示)").

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

澄清实体类型匹配的例外规则。

这里先要求 label 必须完全一致,随后又允许 warning-light 的 Component/Status 跨类型匹配;英文示例还写成了 Status↔Status。请明确该例外是否真实允许,以及仅限哪些 label 组合,避免 LLM judge 产生不稳定结果。

Also applies to: 725-732

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/llm_judge/prompts.py` around lines
675 - 684, The matching rules in prompts.py are inconsistent about the
warning-light exception: the general rule requires exact label equality, but the
special case then permits cross-type matching for Component/Status. Update the
wording around the matching rules section to explicitly state whether this
exception is allowed, and if so, list the exact label combinations it applies
to; also make the example consistent with that rule so the LLM judge behavior is
unambiguous.


precision = matched / cand_count if cand_count > 0 else 0.0
recall = matched / gold_count if gold_count > 0 else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

先修复 Ruff 格式化失败。

CI 已报告该文件 ruff format --check . 不通过;提交前请在仓库根目录运行 uv run ruff format .

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/semantic_entity_f1.py`
at line 122, The current change in semantic_entity_f1.py needs Ruff formatting
cleanup because ruff format --check is failing. Run uv run ruff format . from
the repository root and ensure the formatted result is applied to the
semantic_entity_f1.py logic around the F1 calculation so the file matches Ruff’s
expected style before committing.

Source: Pipeline failures

@@ -0,0 +1,174 @@
# Licensed to the Apache Software Foundation (ASF) under one

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

CI 格式检查失败:需运行 ruff format

流水线报告 ruff format --check . 未通过,需要运行 uv run ruff format . 后重新提交。

As per coding guidelines, "For Python code changes, run root uv run ruff format --check . and uv run ruff check . before handoff."

🧰 Tools
🪛 GitHub Actions: Ruff Code Quality / build (3.10)

[error] 1-1: ruff format --check . reported this file would be reformatted. Run 'uv run ruff format .' to apply formatting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/semantic_triple_f1.py`
at line 1, CI formatting check failed because the Python file is not
Ruff-formatted. Run the repository root formatting command to apply the required
style, then verify the change in semantic_triple_f1.py and any related Python
edits with the same Ruff workflow before handing off. Use the file’s module
content in hugegraph_llm.benchmark.metrics.extraction.semantic_triple_f1 and
ensure the final diff passes the formatting check.

Sources: Coding guidelines, Pipeline failures

Comment on lines +75 to +81
if not prediction and not reference:
return {
"semantic_triple_precision": 0.0,
"semantic_triple_recall": 0.0,
"semantic_triple_f1": 0.0,
"semantic_triple_matched": 0,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

空三元组集合应视为完全匹配,而非 0 分

predictionreference 都为空列表时(即金标准和候选均无三元组),当前返回 precision/recall/f1 全部为 0.0。但语义上这是一次“无遗漏、无误报”的完美匹配,应记为 1.0(或从聚合统计中剔除该样本),而非当作最差情形处理。对于抽取结果本应为空的样本(如纯属性场景),当前实现会系统性拉低整体基准分数。

🐛 建议修复
     if not prediction and not reference:
         return {
-            "semantic_triple_precision": 0.0,
-            "semantic_triple_recall": 0.0,
-            "semantic_triple_f1": 0.0,
+            "semantic_triple_precision": 1.0,
+            "semantic_triple_recall": 1.0,
+            "semantic_triple_f1": 1.0,
             "semantic_triple_matched": 0,
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not prediction and not reference:
return {
"semantic_triple_precision": 0.0,
"semantic_triple_recall": 0.0,
"semantic_triple_f1": 0.0,
"semantic_triple_matched": 0,
}
if not prediction and not reference:
return {
"semantic_triple_precision": 1.0,
"semantic_triple_recall": 1.0,
"semantic_triple_f1": 1.0,
"semantic_triple_matched": 0,
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/semantic_triple_f1.py`
around lines 75 - 81, The empty-triple case in the semantic metric is being
treated as a worst-case miss, but `semantic_triple_f1` should consider
`prediction` and `reference` both empty as a perfect match. Update the
empty-input branch in `semantic_triple_f1` so it returns precision/recall/f1 as
1.0 (with matched count 0 or equivalent) when both lists are empty, while
keeping the existing handling for one-sided empties unchanged. Use the
`semantic_triple_f1` function as the anchor when adjusting this logic.

Comment on lines +99 to +124
matches: List[List[int]] = []
try:
response = retry_llm_call(llm, prompt)
data = _parse_json_response(response)
if data and isinstance(data.get("matches"), list):
matches = [
m for m in data["matches"]
if isinstance(m, list) and len(m) == 2
]
except Exception as e:
logger.warning("Semantic triple matching failed: %s", e)

gold_count = len(gold_lines)
cand_count = len(cand_lines)
matched = len(matches)

precision = matched / cand_count if cand_count > 0 else 0.0
recall = matched / gold_count if gold_count > 0 else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0

return {
"semantic_triple_precision": round(precision, 4),
"semantic_triple_recall": round(recall, 4),
"semantic_triple_f1": round(f1, 4),
"semantic_triple_matched": matched,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

LLM 返回的 matches 未做索引范围与去重校验,可能导致分数虚高

matches 仅校验元素是长度为 2 的列表,未校验 (gold_idx, cand_idx) 是否落在合法范围内,也未去重。若 LLM 输出重复索引对(例如一个候选三元组被判定匹配多个金标准,反之亦然),matched = len(matches) 会被放大,precision = matched / cand_count 理论上可超过 1.0,破坏该指标的可比较性。

🐛 建议修复:按索引去重并校验范围
     matches: List[List[int]] = []
     try:
         response = retry_llm_call(llm, prompt)
         data = _parse_json_response(response)
         if data and isinstance(data.get("matches"), list):
-            matches = [
-                m for m in data["matches"]
-                if isinstance(m, list) and len(m) == 2
-            ]
+            seen_gold: set = set()
+            seen_cand: set = set()
+            valid_matches = []
+            for m in data["matches"]:
+                if not (isinstance(m, list) and len(m) == 2):
+                    continue
+                g_idx, c_idx = m
+                if not (isinstance(g_idx, int) and isinstance(c_idx, int)):
+                    continue
+                if not (0 <= g_idx < len(gold_lines) and 0 <= c_idx < len(cand_lines)):
+                    continue
+                if g_idx in seen_gold or c_idx in seen_cand:
+                    continue
+                seen_gold.add(g_idx)
+                seen_cand.add(c_idx)
+                valid_matches.append(m)
+            matches = valid_matches
     except Exception as e:
         logger.warning("Semantic triple matching failed: %s", e)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
matches: List[List[int]] = []
try:
response = retry_llm_call(llm, prompt)
data = _parse_json_response(response)
if data and isinstance(data.get("matches"), list):
matches = [
m for m in data["matches"]
if isinstance(m, list) and len(m) == 2
]
except Exception as e:
logger.warning("Semantic triple matching failed: %s", e)
gold_count = len(gold_lines)
cand_count = len(cand_lines)
matched = len(matches)
precision = matched / cand_count if cand_count > 0 else 0.0
recall = matched / gold_count if gold_count > 0 else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
return {
"semantic_triple_precision": round(precision, 4),
"semantic_triple_recall": round(recall, 4),
"semantic_triple_f1": round(f1, 4),
"semantic_triple_matched": matched,
}
matches: List[List[int]] = []
try:
response = retry_llm_call(llm, prompt)
data = _parse_json_response(response)
if data and isinstance(data.get("matches"), list):
seen_gold: set = set()
seen_cand: set = set()
valid_matches = []
for m in data["matches"]:
if not (isinstance(m, list) and len(m) == 2):
continue
g_idx, c_idx = m
if not (isinstance(g_idx, int) and isinstance(c_idx, int)):
continue
if not (0 <= g_idx < len(gold_lines) and 0 <= c_idx < len(cand_lines)):
continue
if g_idx in seen_gold or c_idx in seen_cand:
continue
seen_gold.add(g_idx)
seen_cand.add(c_idx)
valid_matches.append(m)
matches = valid_matches
except Exception as e:
logger.warning("Semantic triple matching failed: %s", e)
gold_count = len(gold_lines)
cand_count = len(cand_lines)
matched = len(matches)
precision = matched / cand_count if cand_count > 0 else 0.0
recall = matched / gold_count if gold_count > 0 else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
return {
"semantic_triple_precision": round(precision, 4),
"semantic_triple_recall": round(recall, 4),
"semantic_triple_f1": round(f1, 4),
"semantic_triple_matched": matched,
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hugegraph-llm/src/hugegraph_llm/benchmark/metrics/extraction/semantic_triple_f1.py`
around lines 99 - 124, In the semantic triple F1 metric logic, the current
handling of LLM-returned matches only checks that each entry is a 2-item list,
so duplicate pairs or out-of-range indices can inflate matched counts and make
precision/recall invalid. Update the match normalization in the code path that
uses retry_llm_call and _parse_json_response to validate each (gold_idx,
cand_idx) against gold_lines and cand_lines bounds, deduplicate valid pairs
before counting, and compute matched from the filtered unique set so the
returned semantic_triple_precision, semantic_triple_recall, and
semantic_triple_f1 stay bounded and comparable.

Comment on lines +289 to +317
summarized: List[Dict[str, Any]] = []
for entry in entries:
changes = entry.get(change_key, {})
if not changes:
continue
# worst = most negative semantic delta (regression) or most positive (improvement)
worst_metric, worst_delta = min(changes.items(), key=lambda kv: kv[1]) \
if change_key == "regressions" else max(changes.items(), key=lambda kv: kv[1])
summarized.append(
{
"sample_id": entry["sample_id"],
"question_type": entry.get("question_type"),
"worst_metric": worst_metric,
"worst_delta": worst_delta,
"n_metrics": len(changes),
}
)
summarized.sort(key=lambda r: r["worst_delta"]) # worst first

lines.append("| Sample | 最严重指标 | Δ | 涉及指标数 | 类型 |")
lines.append("|--------|-----------|-----|-----------|------|")
for row in summarized[:limit]:
qt = row["question_type"] or "—"
lines.append(
f"| {row['sample_id']} | {row['worst_metric']} | {_fmt_delta(row['worst_delta'])} "
f"| {row['n_metrics']} | {qt} |"
)
if len(summarized) > limit:
lines.append(f"| ... | 还有 {len(summarized) - limit} 个样例见下方明细 | | | |")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"改进样例"排序方向错误,最大改进被截断到折叠区

_section_samples 对退化样例和改进样例统一使用升序排序 (summarized.sort(key=lambda r: r["worst_delta"]))。对退化样例(worst_delta 为最负值)升序确实能把最严重的排在最前面;但对改进样例,worst_delta 存的是该样本内最大的正向提升,升序排序会把提升幅度最小的样本排在最前,当改进样例数超过 limit=5 时,提升幅度最大的样本反而被挤到"还有 N 个样例见下方明细"的折叠区,未在首屏展示,违背了"按严重程度排序展示 top-N"的设计初衷。

🐛 建议修复
-    summarized.sort(key=lambda r: r["worst_delta"])  # worst first
+    summarized.sort(key=lambda r: r["worst_delta"], reverse=(change_key == "improvements"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
summarized: List[Dict[str, Any]] = []
for entry in entries:
changes = entry.get(change_key, {})
if not changes:
continue
# worst = most negative semantic delta (regression) or most positive (improvement)
worst_metric, worst_delta = min(changes.items(), key=lambda kv: kv[1]) \
if change_key == "regressions" else max(changes.items(), key=lambda kv: kv[1])
summarized.append(
{
"sample_id": entry["sample_id"],
"question_type": entry.get("question_type"),
"worst_metric": worst_metric,
"worst_delta": worst_delta,
"n_metrics": len(changes),
}
)
summarized.sort(key=lambda r: r["worst_delta"]) # worst first
lines.append("| Sample | 最严重指标 | Δ | 涉及指标数 | 类型 |")
lines.append("|--------|-----------|-----|-----------|------|")
for row in summarized[:limit]:
qt = row["question_type"] or "—"
lines.append(
f"| {row['sample_id']} | {row['worst_metric']} | {_fmt_delta(row['worst_delta'])} "
f"| {row['n_metrics']} | {qt} |"
)
if len(summarized) > limit:
lines.append(f"| ... | 还有 {len(summarized) - limit} 个样例见下方明细 | | | |")
summarized: List[Dict[str, Any]] = []
for entry in entries:
changes = entry.get(change_key, {})
if not changes:
continue
# worst = most negative semantic delta (regression) or most positive (improvement)
worst_metric, worst_delta = min(changes.items(), key=lambda kv: kv[1]) \
if change_key == "regressions" else max(changes.items(), key=lambda kv: kv[1])
summarized.append(
{
"sample_id": entry["sample_id"],
"question_type": entry.get("question_type"),
"worst_metric": worst_metric,
"worst_delta": worst_delta,
"n_metrics": len(changes),
}
)
summarized.sort(key=lambda r: r["worst_delta"], reverse=(change_key == "improvements"))
lines.append("| Sample | 最严重指标 | Δ | 涉及指标数 | 类型 |")
lines.append("|--------|-----------|-----|-----------|------|")
for row in summarized[:limit]:
qt = row["question_type"] or "—"
lines.append(
f"| {row['sample_id']} | {row['worst_metric']} | {_fmt_delta(row['worst_delta'])} "
f"| {row['n_metrics']} | {qt} |"
)
if len(summarized) > limit:
lines.append(f"| ... | 还有 {len(summarized) - limit} 个样例见下方明细 | | | |")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hugegraph-llm/src/hugegraph_llm/benchmark/reporters/markdown_reporter.py`
around lines 289 - 317, In `_section_samples`, the sorting direction for
summarized rows is wrong for the “improvements” case:
`summarized.sort(key=lambda r: r["worst_delta"])` works for regressions but
causes the largest improvements to fall out of the top-N. Update the sort logic
in `_section_samples` (using `change_key`, `summarized`, and `worst_delta`) so
regressions stay ascending by most negative delta, while improvements are
ordered to show the largest positive deltas first before applying `limit`.

@Postroggy Postroggy closed this Jul 15, 2026
@Postroggy
Postroggy deleted the feat/graphrag-benchmark-issue7 branch July 15, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Task] GraphRAG 支持轻量 benchmark 评测能力

2 participants