Skip to content

[Issue #74] Add opt-in "enhanced" strategy for schema-based graph extraction - #78

Open
YanhaoLv wants to merge 20 commits into
hugegraph:mainfrom
YanhaoLv:issue74-schema-based-extraction
Open

YanhaoLv wants to merge 20 commits into
hugegraph:mainfrom
YanhaoLv:issue74-schema-based-extraction

Conversation

@YanhaoLv

@YanhaoLv YanhaoLv commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Adds an opt-in enhanced strategy for /graph/extract that applies
a schema-aware quality layer on top of the existing LLM-based
extraction. The current pipeline (renamed here as baseline) stays
byte-compatible: enhanced only runs when the caller sends
extract_strategy: "enhanced".

The new layer runs a candidate parser → schema-aware normalizer →
cross-chunk document assembler → quality gate on every LLM response,
producing structured warnings + a quality-metrics block alongside the
usual vertices / edges.

Closes #74.

Effect

Two independent evaluation tracks — the mock rubric benchmark is for
coverage of every rubric edge case; the live public-corpus benchmark
is for effect evidence under externally-authored ground truth.

Track Baseline F1 Enhanced F1 Delta
Mock rubric benchmark (16 scenarios, FakeLLM, deterministic) 0.86 0.96 +11.6 % rel.
Live DeepSeek on public corpus (8 actors, 3 runs each) 0.418 ± 0.251 0.439 ± 0.188 +4.8 % rel.

Design-stage threshold (baked into a CI assertion inside the mock
benchmark test): enhanced ≥ +5 % relative F1 on the 16-scenario rubric
average. Regressions below this floor fail CI.

Live-track secondary metrics (across all 48 runs):

Metric Baseline Enhanced Delta
F1 std (variance across runs and corpora) 0.251 0.188 −25 %
Wall-clock latency mean per document 16.57 s 14.80 s −10.7 %
Cost per document mean (DeepSeek, cache-miss) $0.003776 $0.003508 −7.1 %
Corpora where enhanced ≥ baseline 5 / 8
Corpora where enhanced regresses 3 / 8

Interpretation. Enhanced is a safety-net strategy: it rescues the
worst cases (Tom Hanks corpus F1: 0.052 → 0.327) and dramatically
narrows the F1 variance (−25 %) while sending +40 % prompt tokens but
producing −24 % completion tokens. Net effect: modest average F1 gain,
much more predictable outcomes, slightly cheaper and faster. It is not
a strict Pareto improvement — on 3 / 8 corpora where baseline was
already strong, enhanced's stricter dedup merges near-duplicate film
titles and regresses F1 by 0.10-0.20. This is documented in the report,
not glossed over.

Full analysis, per-corpus tables, and concrete failure cases (including
the Wikipedia "Thomas Jeffrey Hanks" ↔ Wikidata "Tom Hanks" canonical
name mismatch that hits both strategies) in
docs/quality/schema-based-graph-extract-report.md.

Backwards Compatibility

None broken. Two new optional request fields
(extract_strategy, include_debug, both defaulted) and enhanced-only
meta keys that appear only when the caller opts in. Baseline
responses are byte-identical to the pre-PR shape.

How to Enable

POST /graph/extract
{
  "texts": ["..."],
  "graph_schema": "...",
  "extract_strategy": "enhanced",
  "include_meta": true
}

Full usage guide (API + Python + limitations) at
docs/quality/schema-based-graph-extract-usage.md.

What's in the box

New subpackage hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/:

  • schema_index.py — compiled runtime schema index (label lookup,
    canonical id, property coercion).
  • candidate_parser.py — tolerant JSON extractor covering 5
    LLM-output formats.
  • normalizer.py — per-chunk schema-aware normalizer (property
    filter → coerce → PK check → canonical id).
  • document_assembler.py — cross-chunk alias-table union with
    ambiguity detection and endpoint repair.
  • quality_gate.py — 11-metric quality aggregate per run.
  • warnings.py — structured warning registry (20 codes).
  • evaluator.py — offline evaluator (precision/recall/F1 + property
    fidelity metrics).
  • prompt_contract.py — schema-derived constraint block that
    enhanced appends to the LLM prompt.

Threaded through the existing pipeline:
api/graph_extract_api.py, flows/graph_extract.py,
operators/llm_op/property_graph_extract.py, state/ai_state.py,
nodes/llm_node/extract_info.py.

Public-corpus builder + live benchmark driver (outside CI, network
required):

  • scripts/build_public_actor_corpus.py — fetches Wikipedia lead
    extracts + verifies GT against Wikidata P161 (cast member) claims
    via the entity API. Output pinned to
    hugegraph-llm/src/tests/data/public_actor_corpus.json.
  • scripts/graph_extract_live_benchmark.py — accepts --corpus
    (required, no hand-authored fallback) and --runs; produces the
    mean ± std, per-corpus, per-strategy JSON archive.
  • Live-benchmark raw archive (~600 KB, 48-run record including every
    LLM call's prompt/completion tokens, latency, and raw predicted
    output) is not bundled in this PR to keep the diff reviewable —
    the archive alone would add ~21 k lines (~67 % of the total diff).
    Available on request; sha256:
    8c7b7a8c22451405d9a6f4403dae09a534777c097a396cfcb4e563fc9e04b1e7.
    Every number in the effect report's live section is jq-derivable
    from the archive (queries in the report's "How to Reproduce This
    Report" section).

Tests (all unit or contract, deterministic):

  • test_property_graph_schema.py
  • test_property_graph_warnings.py
  • test_property_graph_postprocess.py (parser + normalizer + assembler)
  • test_property_graph_quality_gate.py
  • test_property_graph_evaluator.py
  • test_property_graph_benchmark.py (16 rubric scenarios, incl.
    s15/s16 domain-limit cases where enhanced ≡ baseline)
  • test_property_graph_extract.py (extended for enhanced dispatch)
  • test_graph_extract_api.py (extended for API-layer meta)

Test Plan

  • 398 unit/contract tests pass locally.
  • Enhanced package coverage: 95 % (evaluator 94 %, normalizer 96 %,
    parser 97 %, assembler 90 %, quality_gate 100 %, warnings 100 %).
  • Full repo coverage 46.31 % on the CI-equivalent
    -m "unit or contract" filter, well above the 34 % CI floor.
  • Ruff check + ruff format — clean.
  • Baseline byte-compatibility: dedicated API-level assertion that
    baseline meta remains {vertex_count, edge_count, text_count}.
  • Design invariant asserted by the benchmark test: enhanced F1
    ≥ baseline F1 on every scenario.
  • Design-stage threshold asserted in the same test: mock benchmark
    avg F1 gain ≥ +5 % relative. Actual: +11.6 %.
  • Live DeepSeek run recorded end-to-end (F1 mean/std/min/max,
    latency, tokens, cost) for all 8 corpora × 3 runs = 48 samples.

Scope Notes

  • Person canonical name mismatch hits both strategies on all live
    corpora and is the biggest single F1 loss source. Wikipedia leads
    say "Thomas Jeffrey Hanks"; Wikidata canonicals say "Tom Hanks". No
    schema-only pipeline can bridge that without either LLM alignment or
    an entity-resolution step. Both are out of scope for Issue [Task] 效果优先的 Schema-based 图抽取功能实现 #74. This
    is documented as a known limitation in the usage doc, not hidden.
  • The token_usage field in meta remains a placeholder pending an
    LLM adapter change to surface token metadata through BaseLLM. The
    live benchmark script tracks tokens by calling the LiteLLM completion
    API directly.
  • Set-based F1 was chosen so that baseline's cross-chunk vertex
    duplication doesn't get double-counted; raw counts are still exposed
    in ItemMetrics.predicted_count_raw for downstream commit-load
    monitoring.

Summary by CodeRabbit

  • 新功能

    • 新增可选的“增强”图抽取模式,支持更严格的 schema 约束、结构化告警、调试信息和质量指标。
    • 增加了图抽取结果的跨分块合并、端点修复和重复项去重能力。
  • 文档

    • 更新了抽取用法说明、评测报告和迁移指南,补充了参数、返回字段和已知限制。
  • 测试

    • 增加了覆盖 schema、告警、质量指标和端到端抽取流程的测试,并加入基准对比验证。

YanhaoLv added 20 commits July 7, 2026 11:31
…ine)

Introduce a request-level extract_strategy ("baseline" | "enhanced") flag
and an include_debug flag, threaded through GraphExtractRequest → flow
prepare → WkFlowInput → ExtractNode → PropertyGraphExtract. Baseline is
byte-compatible: the flag is only surfaced in the shared context, and the
existing chunk-level parse/normalize path runs unchanged.

Subsequent commits introduce the schema-aware graph quality layer behind
the enhanced flag (parser → normalizer → document-level assembler →
quality gate → structured warnings). For this commit only the plumbing
lands; enhanced still falls through to baseline logic and emits an INFO
log noting the layer is not yet wired.

- Pydantic Literal validation returns 422 on unknown extract_strategy.
- Unknown values passed via internal constructors are normalized to
  baseline (defence in depth against non-API entry points).
- WkFlowInput.reset() and WkFlowState.setup() now clear the new fields to
  prevent state leakage across pooled pipeline reuse.
- Tests cover: request defaults, explicit baseline/enhanced acceptance,
  unknown-strategy rejection, scheduler kwargs pass-through, flow prepare
  capture/defaults, WkFlowInput reset, PropertyGraphExtract constructor
  captures / normalizes strategy, and run() surfaces resolved strategy
  in the returned context.
Add a runtime schema index for the enhanced graph extraction strategy.
GraphSchemaIndex compiles a validated HugeGraph schema (dict or JSON
string) into deterministic lookups used by the schema-aware quality
layer: vertex/edge label membership, allowed property sets, property
data-type and cardinality queries, canonical vertex id generation, safe
property value coercion, and edge endpoint compatibility.

The canonical id rule ({vertex_label.id}:{pk1}!{pk2}) intentionally
mirrors PropertyGraphExtract._primary_key_id — the enhanced strategy
inherits baseline behavior including the None fallback for schemas
without id_strategy=PRIMARY_KEY or a populated vertex_label.id. This
keeps the write-to-graph and baseline evaluator paths byte-compatible.

Coercion rules follow the design contract:
- TEXT/UUID/BLOB: string passthrough (str() for non-strings).
- INT/LONG/BYTE: strict integer only; rejects booleans and lossy floats.
- FLOAT/DOUBLE: numeric or parseable string; rejects booleans and
  non-numeric strings.
- BOOLEAN: accepts bool, "true"/"false"/"yes"/"no"/"1"/"0", or 0/1 int.
- DATE: only YYYY-MM-DD; no fuzzy parsing.
- LIST/SET: element-wise coercion with partial-drop warning; SET
  additionally deduplicates.

The module is pure additive — no existing file is touched. It performs
no HugeGraph I/O and does not resolve named-graph schema strings.
Callers must hand in the concrete schema (SchemaNode already does).

50 unit tests cover: constructor validation (dict/JSON/rejects), label
lookups, property allowlists and type/cardinality queries, single and
multi primary-key canonical ids, missing/empty PK values, CUSTOMIZE
id_strategy, schemas without vertex_label.id, edge endpoint spec and
direction check, each data-type coercion path (success and failure),
and LIST/SET cardinality including partial-drop and deduplication.
Introduce the structured warning surface for the enhanced graph extraction
strategy. Every material deviation from the schema-clean happy path (a
dropped item, a coerced property value, a merged duplicate, an
unresolvable endpoint) becomes a StructuredWarning that will flow into
meta.structured_warnings, the effect report, and the debug payload.

WarningCode is a (str, Enum) subclass so codes JSON-serialize as bare
strings without a custom encoder. Codes are grouped by producer:

- Candidate parser: JSON_NOT_FOUND, JSON_DECODE_FAILED,
  GRAPH_SECTION_MISSING, ITEM_NOT_OBJECT, ITEM_TYPE_MISMATCH
- Normalizer (vertex): VERTEX_LABEL_NOT_IN_SCHEMA,
  VERTEX_PRIMARY_KEY_MISSING, VERTEX_PRIMARY_KEY_INVALID,
  VERTEX_ALIAS_RECORDED
- Normalizer (edge): EDGE_LABEL_NOT_IN_SCHEMA, EDGE_ENDPOINT_MISMATCH
- Property: PROPERTY_NOT_IN_SCHEMA, PROPERTY_COERCED,
  PROPERTY_COERCION_FAILED
- Document assembler: ENDPOINT_PENDING_REPAIR, ENDPOINT_UNRESOLVED,
  ENDPOINT_AMBIGUOUS, DUPLICATE_VERTEX_MERGED, DUPLICATE_EDGE_MERGED,
  PROPERTY_CONFLICT

StructuredWarning is a frozen dataclass, hashable, defensively copies
its context mapping, and rejects invalid item_type or negative chunk_id
at construction. is_surface_affecting distinguishes codes that
represent a real change to the emitted graph from purely observational
markers (alias recorded, pending repair, first-wins conflict), which
the effect report uses to explain why item counts changed.

warning_code_distribution() aggregates a warning list into a
{code: count} histogram, silently skipping foreign entries so a
malformed tail can never sink the effect report.

22 unit tests cover: enum surface, JSON encoding without custom
encoder, dataclass validation and immutability, defensive context
copy, to_dict serialization (full and None-omitting shapes),
is_surface_affecting classification, and the distribution helper.
Parse a single chunk's raw LLM output into a CandidateGraph plus a list
of StructuredWarning. Handles the five formats produced by different
prompt styles:

- grouped JSON object: {"vertices": [...], "edges": [...]}
- Markdown-fenced JSON (```json ... ``` or bare ```)
- JSON wrapped in surrounding prose
- flat array: [{"type": "vertex", ...}, {"type": "edge", ...}]
- explicit-type items inside grouped sections

JSON discovery is layered: json.loads on the stripped text first (fast
path when the LLM emits clean JSON), then greedy {...} and [...]
extraction for the wrapped-in-prose case. A failed attempt with any
opening brace or bracket present yields JSON_DECODE_FAILED so callers
can distinguish "LLM tried and produced garbage" from "LLM emitted
prose only" (JSON_NOT_FOUND).

Item collection drops non-dict candidates (ITEM_NOT_OBJECT), items in
flat-array position without a routable type (ITEM_TYPE_MISMATCH), and
items in grouped sections whose explicit type disagrees with the
containing array (ITEM_TYPE_MISMATCH). Missing vertices/edges sections
in a grouped payload each produce a GRAPH_SECTION_MISSING warning; the
missing side is normalized to an empty list so downstream stages can
still run.

Item type is normalized to the expected value on every kept candidate
so the normalizer never needs a defensive default. Original caller
dicts are not mutated (each kept item is a shallow copy).

Also introduces types.py holding the shared CandidateGraph dataclass
(frozen, default_factory-safe lists, is_empty helper) that the
normalizer and assembler will consume in the next commits.

27 unit tests cover: the dataclass shape, all five input formats (with
and without fences), prose surrounding JSON, empty and whitespace-only
inputs, missing/non-list sections, flat-array items without type,
non-dict items in both formats, truncated JSON, scalar payloads,
grouped explicit-type conflicts, chunk_id propagation, item type
normalization, and immutability of caller-side dicts.
Consume a CandidateGraph plus a GraphSchemaIndex and produce a
NormalizedChunkGraph in which every vertex is schema-valid with
canonical ids when possible, every edge has both endpoints resolved
(or explicit pending hints for the document-level assembler), and
every deviation is recorded as a StructuredWarning tagged with the
originating chunk_id.

Processing order (per design section 6.4):

- Vertex: label ∈ schema? → filter properties by allowed_properties →
  coerce values against propertykeys[].data_type / .cardinality →
  primary-key completeness check → canonical_vertex_id.
- Edge: label ∈ schema? → filter properties → resolve out/in labels
  (falling back to the schema's edge spec when the LLM omits them) →
  is_endpoint_compatible → resolve endpoints via (a) legacy source/
  target dicts against the schema, (b) explicit outV/inV against the
  chunk's alias table.

An edge that can be neither fully resolved at chunk level nor cleanly
ruled out is emitted with _pending_out / _pending_in hints for the
assembler, plus an ENDPOINT_PENDING_REPAIR warning. Edges with
schema-invalid endpoint directions are dropped immediately with
EDGE_ENDPOINT_MISMATCH.

The vertex alias table is seeded with identity entries in addition to
(original_id → canonical_id) so downstream edges can resolve
endpoints regardless of whether the LLM referenced a vertex by its
raw id or its canonical form. Property coercion emits the soft
PROPERTY_COERCED warning only when the value's Python type actually
changed — same-type same-value coerces stay silent to avoid drowning
the report.

Also adds NormalizedChunkGraph, PENDING_OUT_KEY, PENDING_IN_KEY to
the shared types module. Frozen dataclass with default_factory lists
and dict — mutation of the collections is allowed but rebinding the
fields is not, matching CandidateGraph's contract.

29 unit tests cover: vertex label validity and drop, property filter,
type coercion (soft warning on type change, non-PK failure drops
property, PK failure drops vertex), primary-key completeness (missing
or empty), LLM-original-id alias recording (including the no-op case
where original matches canonical), edge label validity, edge
property filter, endpoint direction check and schema-fill fallback,
legacy source/target resolution, chunk-alias resolution, unresolvable
endpoint marking, and an end-to-end parser+normalizer integration
smoke that verifies the two work together on a realistic grouped
payload.
Assemble every chunk's NormalizedChunkGraph into a final DocumentGraph:
cross-chunk vertex merge, document-level endpoint repair, and edge
deduplication. This is the last stage the enhanced strategy needs
before the graph reaches the API envelope.

Vertex merge (per design section 6.5):
- Key: (label, id). First-appearance wins for merge target and
  conflicting property values.
- Non-conflicting properties from later occurrences are folded in.
- Property conflicts emit PROPERTY_CONFLICT (soft — the first value
  stays); every merge event emits DUPLICATE_VERTEX_MERGED so the
  quality gate can count exact duplicates.
- Vertices without an id are kept verbatim (no way to identify a
  merge partner).
- Output order preserves first appearance for a deterministic result.

Endpoint repair:
- Union every chunk's alias table into a document-level index.
  Conflicting mappings for the same (label, key) are tracked in an
  ambiguous set — resolving through such a key yields
  ENDPOINT_AMBIGUOUS rather than silently picking one candidate.
- Edges the normalizer left pending (with _pending_out / _pending_in
  hints) are resolved via the merged alias index. Successful
  repairs bump endpoint_repair_count on the resulting DocumentGraph.
- Unresolvable pending endpoints drop the edge with
  ENDPOINT_UNRESOLVED; ambiguous ones drop with ENDPOINT_AMBIGUOUS.

Edge dedup:
- Key: (label, outVLabel, outV, inVLabel, inV, properties_signature).
  Property signatures use json.dumps(sort_keys=True) so LIST/SET
  cardinality property values (unhashable when tupled) still dedupe.
- Edges with the same endpoints but different property signatures are
  both kept — losing a distinct fact is worse than surfacing a near
  duplicate.
- Emits DUPLICATE_EDGE_MERGED per merge event, preserves first-
  appearance order.

Also introduces DocumentGraph in the shared types module. Beyond
vertices and edges, the dataclass carries pre_merge_vertex_count,
pre_merge_edge_count, and endpoint_repair_count so the quality gate
(next commit) can compute duplicate reduction and repair-rate metrics
without re-scanning inputs.

15 unit tests cover: vertex merge across chunks, property conflict
first-wins, missing property completion from later chunks, distinct
keys stay separate, output ordering, no-id vertex handling,
cross-chunk pending-endpoint repair, unresolved and ambiguous drops,
resolved-at-chunk-level pass-through, identical edge dedup, distinct-
property-signature preservation, and LIST-property dedup safety. Two
integration tests drive parser + normalizer + assembler end-to-end.
Aggregate the structured warnings emitted throughout the enhanced
pipeline plus the counts tracked by DocumentGraph into a single
QualityMetrics bundle. Powers the meta.quality_metrics field on the
API response and feeds the baseline-vs-enhanced comparison report.

Must metrics per design section 6.6:
- schema_valid_vertex_ratio / schema_valid_edge_ratio — fraction of
  candidate items that survived normalization.
- endpoint_resolution_rate — pending edges resolved by the assembler
  vs. pending edges surfaced by the normalizer.
- duplicate_vertex_reduction / duplicate_edge_reduction — merge/dedup
  effectiveness across chunks.
- property_valid_ratio — kept properties / (kept + invalidated). PROPERTY_
  CONFLICT is *not* counted as invalid because the first value survives.
- dropped_item_count — sum of vertex/edge/item/property drop codes.
  Excludes DUPLICATE_*_MERGED (consolidated, not dropped) and
  PROPERTY_COERCED (value changed, item survived).
- coerced_property_count, endpoint_repair_count — flow-through counters.

Should metrics that fell out of the same aggregation cheaply:
- property_conflict_count.
- warning_code_distribution (delegated to warnings.warning_code_
  distribution helper).

Zero-input safety is a design contract, not an accident: every ratio
returns a plain float in [0, 1] regardless of empty inputs. Ratios
where "no candidates, no problems" is a passing state default to 1.0;
reduction ratios (where "nothing to reduce" reads as "no reduction
achieved") default to 0.0. NaN cannot escape the gate.

QualityMetrics is a frozen dataclass; to_dict rounds ratios to 4
decimals so the API response is compact and floating-point-stable.
Non-StructuredWarning entries in the warning list are silently
skipped so a malformed tail at the very end of a long pipeline
cannot sink the report.

26 unit tests cover: zero-input safety (all ratios in [0,1], no NaN,
correct defaults), schema-validity ratios with normalization drops,
endpoint resolution (full/partial/ambiguous/zero-pending), duplicate
reduction, property validity (kept/dropped/conflict-doesn't-count),
counter sums (drop codes / duplicates-and-coercions excluded /
coerced count / repair count from graph), warning code distribution,
serialization (rounding, JSON round-trip, frozen enforcement),
robustness against malformed warning entries, and a realistic
multi-metric integration scenario.
Activate the schema-aware graph quality layer behind
extract_strategy="enhanced". Requests continue to default to baseline
and stay byte-compatible; only an explicit opt-in reaches the new
pipeline.

Enhanced path per chunk:
  raw LLM output
    → CandidateGraphParser.parse
    → SchemaAwareNormalizer.normalize (with GraphSchemaIndex)
    → NormalizedChunkGraph
Then across all chunks:
    → DocumentGraphAssembler.assemble
    → GraphQualityGate.compute
The final DocumentGraph vertices and edges replace context["vertices"]
and context["edges"]. Warnings from every stage accumulate into
context["structured_warnings"]; quality metrics land in
context["quality_metrics"]; when include_debug=true a per-chunk
diagnostic block (raw output truncated at 2 KB, candidate/normalized
counts, per-chunk warning count) plus the warning-code distribution
land in context["debug_info"].

New prompt_contract module:
- build_prompt_contract(schema_index) returns a short structural
  constraint block enumerating the schema's vertex and edge labels
  plus the six rules from design section 6.1 (schema-only labels,
  schema-only properties, PK-required, endpoint forms, omit-not-
  fabricate, JSON-only output).
- Appended after example_prompt so caller-supplied framing keeps
  its shape; the downstream quality layer remains the effective
  guarantor of output validity.

Flow post_deal now surfaces enhanced-only fields (extract_strategy,
chunk_count, call_count, structured_warnings, quality_metrics,
debug_info) on top of the baseline JSON envelope. Baseline output
stays exactly as before (unchanged log message, unchanged payload
shape) so existing callers see no diff.

API layer routes those fields into meta:
- meta always carries the three baseline counts when
  include_meta=true;
- when strategy=="enhanced": additionally extract_strategy,
  chunk_count, call_count, token_usage="unavailable",
  structured_warnings, quality_metrics;
- when include_debug=true: debug_info lands in meta regardless of
  include_meta (matches design section 7.3);
- top-level warnings[] gains a short "N structured warning(s)"
  summary so legacy callers reading only warnings see the enhanced
  activity signal.

WkFlowState grows four enhanced-output fields
(chunk_count / structured_warnings / quality_metrics / debug_info)
plus setup() clears them so pipeline-pool reuse cannot leak state
across requests.

Tests: 8 new enhanced-strategy tests on PropertyGraphExtract
(full-pipeline canonical ids, prompt appends constraint block,
schema-invalid item drop with warning, cross-chunk dedup, include_
debug records, quality_metrics shape). 4 new API-layer tests
(enhanced meta carries strategy+chunk+call+token_usage; structured_
warnings + quality_metrics reach meta; debug_info gated on include_
debug; baseline meta byte-compat regression). Full test suite is
green at 562 passed with coverage at 55.50% (floor 34%).
Offline evaluator that scores a predicted property graph against a
labelled reference using set-based precision/recall/F1 (matched by
(label, id) for vertices, (label, outV, inV) for edges) plus property
fidelity metrics (valid_ratio, exact_match_rate). Empty-graph edge
cases match standard conventions: both empty → 1.0, one-sided empty
degrades the affected component.

Ten deterministic scenarios drive both extraction strategies through
the same FakeLLM per-chunk responses so their outputs can be compared
apples-to-apples. Design invariants are asserted: enhanced never
regresses F1 relative to baseline, and enhanced wins strictly on
cross-chunk edges, alias mismatch, and INT coercion.

Report at docs/quality/schema-based-graph-extract-report.md captures
the numeric results (baseline avg F1 0.84 → enhanced 1.00, +0.16).
Numbers are reproducible via
'uv run --directory hugegraph-llm pytest .../test_property_graph_benchmark.py -s'.

Also adds pytestmark = pytest.mark.contract to the schema/warnings/
quality_gate/postprocess/evaluator/benchmark test modules so CI's
'-m "unit or contract"' filter picks them up.

Coverage of the enhanced package now sits at 95% (evaluator 94%);
392 unit/contract tests green.
…dd usage doc

Adds four scenarios that close the rubric coverage matrix:
- s11 missing endpoint referent (edge points at a vertex no chunk defines)
- s12 wrong edge direction (Movie -> Person against schema Person -> Movie)
- s13 duplicate edge across chunks (enhanced dedupes at raw level)
- s14 multi-primary-key vertex (Employee keyed by (name, company))

Benchmark now measures per-scenario post-LLM latency and asserts LLM
call count equals chunk count for both strategies. Table gains latency,
call-count, and raw-vertex-count columns; explicit F1 relative-gain
line at the footer.

Failure-case analysis in the effect report replaces the terse
"where enhanced wins" note with concrete LLM outputs + baseline vs
enhanced behavior + production impact for scenarios s03/s04/s06/s07/s10.

Adds docs/quality/schema-based-graph-extract-usage.md covering opt-in
via API and Python, response meta layout, structured-warning codes,
applicable scenarios, and known limitations (prompt-token overhead,
best-effort coercion, no cross-request state).

14-scenario benchmark: baseline avg F1 0.89 -> enhanced 1.00 (+12.9%
relative); property_exact_match_rate 0.93 -> 1.00; enhanced never
regresses. 396 unit/contract tests pass; enhanced package coverage
stays at 95%.
…ction

scripts/graph_extract_live_benchmark.py runs both baseline and enhanced
strategies against DeepSeek Chat on a 3-chunk Tom Hanks corpus with a
labelled ground truth, capturing wall-clock latency, per-call token
usage, and an approximate USD cost (rate card 2026-07). Full run
archive lands at .workflow/deepseek_live_run.json (gitignored).

Effect report gains a Live LLM Benchmark section with:
- Aggregate table (F1, match rate, latency, tokens, cost).
- Per-call breakdown so token overhead of enhanced's constraint block is
  visible per chunk (baseline ~413 vs enhanced ~640 prompt tokens/call).
- Failure analysis on the two baseline losses in this run: DeepSeek
  emitted the character 'Chuck Noland' and the pronoun 'He' as Persons.
  Baseline propagated both; enhanced's constraint block nudged DeepSeek
  to drop them, and the cross-chunk assembler linked chunk 3's Woody
  edge back to Tom Hanks from chunk 1.
- Cost/quality trade-off discussion (+12.5% cost, +33.3% F1 relative).

pyproject.toml: allow T20 (print) in scripts/ so the CLI benchmark
passes ruff.

Live DeepSeek results (single run, temp 0.0):
- baseline: F1 0.75, 5.44s, $0.000987
- enhanced: F1 1.00, 5.22s, $0.001110

Mock benchmark unchanged; 396 unit/contract tests still pass.
Two new scenarios covering cases where neither strategy can win:

- s15 character-promoted-to-person. LLM emits "Chuck Noland" (a fictional
  role from Cast Away) as a first-class Person vertex. The name is
  schema-valid; neither strategy has world knowledge to catch it.
  Both F1 caps at 0.86. Reproduces the live DeepSeek failure mode.

- s16 pronoun-ghost-no-prior-context. Single-chunk "He voiced Woody in
  Toy Story." Enhanced's document assembler needs a prior chunk to
  bind "He" to; without one it emits the ghost Person and its
  spurious outgoing edge. Both F1 caps at 0.50.

Two dedicated tests assert baseline F1 == enhanced F1 on these, so the
"enhanced ties baseline on domain limits" claim is machine-checked, not
just prose in the report.

Design threshold DESIGN_F1_RELATIVE_GAIN_MIN = 0.05 is now asserted in
test_benchmark_produces_comparison_table: mock average F1 gain must be
>= +5% relative or CI fails. Regressions can no longer sneak in as a
number-check in the report; they break the test.

Mock benchmark aggregate moves from 14 scenarios (baseline 0.89 ->
enhanced 1.00, +12.9%) to 16 scenarios (baseline 0.86 -> enhanced 0.96,
+11.6% relative). The lower enhanced ceiling is deliberate: it prevents
the report from claiming "enhanced always reaches 1.00" -- a claim that
would be dishonest given the live-track ceiling of 0.72.

398 unit/contract tests pass (was 396 with the +2 new assertions).
…ty API)

The earlier live benchmark used a hand-authored 3-chunk Tom Hanks
corpus with a hand-authored 7-item ground truth. That evaluation
suffered from two-layer selection bias: the same person picked the
text (choosing spots where baseline was known to fail) AND wrote the
answer key. F1=1.00 on that corpus is not defensible as effect evidence.

This script builds a reproducible public corpus instead:

- Text source: Wikipedia lead extract (via MediaWiki API
  action=query prop=extracts exintro), pinned by article revid.
- Ground truth source: Wikidata P161 (cast member) claims verified
  per film via wbgetentities. Only films whose entity is instance-of
  (P31) a film class AND whose P161 lists the actor's Q-id are
  admitted; TV shows, franchises, and disambiguation-wrong Q-ids are
  filtered out and recorded in rejected_mentions for auditability.
- No SPARQL: Wikidata Query Service is currently under an outage that
  rate-limits SPARQL to 1 req/min. The Entity API (wbsearchentities +
  wbgetentities) is separately available and covers this workflow.

Politeness/robustness:
- Wikimedia-compliant User-Agent with contact URL.
- 5s inter-request pacing on Wikidata (429s during the outage are
  aggressive; empirically this rate works).
- 30s -> 60s -> 90s -> 120s -> 150s backoff on 429.
- Per-actor cap of 15 candidate films to bound runtime (~15 min total
  for 8 actors).
- Cross-actor Q-id cache (film titles overlap across actors' bios).

Output: hugegraph-llm/src/tests/data/public_actor_corpus.json with
per-corpus wikipedia_url, actor_qid, verified_films, rejected_mentions
so a reviewer can independently audit every admit/reject decision.
Frozen output of scripts/build_public_actor_corpus.py at build time
2026-07-05. Committed so live benchmark reruns produce comparable
numbers without needing a fresh Wikipedia/Wikidata fetch (which is
sensitive to article rev changes and Wikidata rate limits during the
current WDQS outage).

Contents:
- 8 actors: Tom Hanks, Meryl Streep, Leonardo DiCaprio,
  Denzel Washington, Julia Roberts, Anthony Hopkins, Nicole Kidman,
  Morgan Freeman.
- 24 chunks (3 per actor, sentence-boundary split at ~800 chars).
- 65 GT vertices, 57 GT edges (all Person -> Movie ACTED_IN).
- Per-corpus wikipedia_revision, wikipedia_url (permalink),
  actor_qid, verified_films list, rejected_mentions list with
  reason codes.

Text is CC-BY-SA (Wikipedia); attribution is preserved via the
wikipedia_url field per corpus entry. Ground truth is Wikidata
(CC0). No PII, no author-fabricated content.

To rebuild: python scripts/build_public_actor_corpus.py --output
hugegraph-llm/src/tests/data/public_actor_corpus.json. Rebuild will
differ if the underlying Wikipedia articles have been edited since
the pinned revids.
…aggregation

New CLI:
  python scripts/graph_extract_live_benchmark.py \
    --corpus <path-to-public_actor_corpus.json> \
    --runs 3 \
    --output <archive.json>

Legacy behavior preserved for callers who omit --corpus: the previous
hand-authored 3-chunk Tom Hanks CORPUS + GROUND_TRUTH is kept as a
smoke/sanity sample (name: legacy_tom_hanks_3chunk).

Aggregation shape:
- Every (corpus, strategy, run) tuple stored individually in the
  archive under "runs" so per-call token usage and raw predicted
  output stay auditable.
- per_corpus_strategy_aggregation: {mean, std, min, max, n} over
  runs for F1 / vertex F1 / edge F1 / property match / latency /
  tokens / cost.
- per_strategy_aggregation: same, pooled across all corpora.
- delta.f1_absolute and delta.f1_relative_percent.

Why multi-run at temperature 0.0: DeepSeek at temp=0 is
near-deterministic but not fully; token counts vary a few percent
across identical requests due to server-side batching. std across 3
runs of the same (corpus, strategy) turns out non-trivial for a few
corpora, so multi-run is not vestigial.

Public-corpus results on 8 actors x 3 runs = 48 samples:
- baseline F1 = 0.418 +/- 0.251, enhanced F1 = 0.439 +/- 0.188
- delta: +0.020 absolute (+4.8% relative)
- latency: 16.57s -> 14.80s (-10.7%)
- cost/document: $0.003776 -> $0.003508 (-7.1%)
- enhanced >= baseline on 5/8 corpora, regresses on 3/8

Numbers documented in
docs/quality/schema-based-graph-extract-report.md (Live LLM Benchmark
on Public Corpus section) in the docs commit that follows.
…hmark

An earlier revision of this report leaned on a hand-authored 3-chunk
Tom Hanks corpus with a hand-authored 7-item ground truth and reported
F1=1.00 on enhanced (+33.3% relative vs baseline). That evaluation
suffered from two-layer selection bias -- the same person picked the
text and wrote the answer key. The +33.3% number is not defensible as
effect evidence.

This commit rewrites the report on top of:
- Mock rubric benchmark (16 scenarios, deterministic FakeLLM). Serves
  as rubric coverage. Enhanced 0.86 -> 0.96, +11.6% relative.
- Live DeepSeek benchmark on public corpus (8 Wikipedia articles,
  Wikidata-verified GT, 3 runs each = 48 samples). Serves as effect
  evidence. Enhanced 0.418 +/- 0.251 -> 0.439 +/- 0.188, +4.8%
  relative.

The public-corpus numbers are much smaller than the previous hand-
authored numbers. That gap is the price of an honest evaluation. Real
effect is:
- Modest F1 gain (+4.8% relative average)
- Substantial variance reduction (-25% std)
- Big worst-case rescue (Tom Hanks: 0.052 -> 0.327)
- Latency down 10.7%, cost/document down 7.1%
- Enhanced regresses on 3 of 8 corpora where baseline was already
  strong (Julia Roberts, Denzel Washington, Morgan Freeman)

New sections:
- Evaluation Methodology: explains why the two tracks exist and what
  each measures.
- Live corpus provenance: Wikipedia REST + Wikidata Entity API
  workflow; per-actor stats table with rejection counts.
- Per-corpus breakdown: shows the 5/3 win/loss split explicitly.
- Live failure case analysis: two dominant failure modes documented
  (Person canonical name mismatch across bio-style writing, LLM
  extracting films Wikidata hasn't cast-tagged).
- Expanded Scope and Caveats: strict-GT limits, WDQS-outage note,
  sample-size discussion.

Usage doc gains:
- "No knowledge-base entity resolution" limitation entry describing
  the Wikipedia "Thomas Jeffrey Hanks" vs Wikidata "Tom Hanks"
  canonical alias problem both strategies hit.
- "When enhanced does NOT help" section describing the 3/8 regression
  pattern.

Design threshold for enhanced (>= +5% relative F1 average on the
16-scenario mock benchmark) is now baked into a CI assertion; the
report references it rather than repeating the number.
…driver

The live benchmark script previously accepted --corpus as optional and
fell back to a hard-coded 3-chunk Tom Hanks corpus with hand-authored
ground truth when the flag was omitted. That fallback contradicts the
public-corpus track's narrative: the whole point of moving to Wikipedia
lead + Wikidata P161 GT was to remove selection bias, and keeping an
in-script hand-authored corpus silently reintroduces it if anyone runs
the script without --corpus.

Changes:
- Delete the hard-coded CORPUS list and GROUND_TRUTH dict.
- Make --corpus required; argparse now rejects missing values.
- Update module docstring to explain the deliberate absence of a
  fallback so future maintainers do not "helpfully" re-add one.

The committed public corpus at
hugegraph-llm/src/tests/data/public_actor_corpus.json is now the only
input path.
Adds the full 48-run archive (8 corpora x 2 strategies x 3 runs) from
the live DeepSeek benchmark. Previously this file lived only in
.workflow/, which is repo-locally excluded — so reviewers looking at
the effect report could see the aggregated numbers but had no way to
cross-check them against per-run data.

The archive contains, for each of the 48 runs: F1 (overall/vertex/edge),
property match rate, per-LLM-call prompt/completion tokens and latency,
predicted vertices/edges, and the full evaluator output. Aggregated
mean/std/min/max blocks per (corpus, strategy) and per strategy are
included at the top level.

Reviewers can jq every number in the report's live-track section
directly from this file — see the new "How to Reproduce This Report"
section for the exact queries.

The file is 612 KB. It contains no API keys, no raw LLM prompt or
completion text — only token counts, latencies, and post-LLM parsed
graph output. Model identifier "deepseek/deepseek-chat" and cache-miss
pricing are publicly known.
…tionale

Two additions to the schema-based-graph-extract-report:

1. New "Why the live delta (+4.8%) is smaller than the mock delta
   (+11.6%)" subsection in the executive summary. Explains, in four
   parts:
   - The number is smaller than the previous (+33.3%) claim because
     that claim was double-selection-biased; +4.8% is what survives
     honest measurement.
   - The F1 mean is the least-interesting metric in the results —
     variance reduction (-25%), worst-case improvement (4.4x),
     latency (-11%), and cost (-7%) are the load-bearing claims.
   - Mock (+11.6%) measures the pipeline in isolation; live (+4.8%)
     measures pipeline + LLM. Both numbers are real; they measure
     different things.
   - The +5% CI threshold is asserted on the mock track, not the
     live track, because live is subject to server-side variance and
     Wikidata-GT completeness limits that would either loosen the
     threshold to uselessness or tighten it below normal noise.

2. New top-level "How to Reproduce This Report" section listing every
   committed artifact (public corpus JSON, live-benchmark archive JSON,
   builder script, driver script) with paths, plus the exact
   commands to regenerate each track. Includes three jq queries that
   let a reviewer cross-check every live-track number against the
   committed archive without re-running the benchmark.

The two per-track "Reproducing X" subsections that were duplicating
this material have been shortened to backlinks.

No numbers changed.
Move the 48-run live-benchmark archive out of the PR to shrink the
diff. The archive was ~600 KB / 21 k lines, roughly 67% of the total
change; removing it lets reviewers focus on the actual feature code
and tests.

All numbers in the effect report remain valid. The archive is
available on request; report now includes sha256 for integrity
verification, and the jq examples still describe how to derive every
number once the archive is obtained.

sha256(live_benchmark_public_actors.json) =
  8c7b7a8c22451405d9a6f4403dae09a534777c097a396cfcb4e563fc9e04b1e7
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

@codecov-ai-reviewer review

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

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

本 PR 新增可选启用(opt-in)的 schema-aware 增强图抽取策略,包含候选解析、模式归一化、文档级跨 chunk 组装、质量门控与离线评估器等纯 Python 组件,并将其接入 PropertyGraphExtract、GraphExtractFlow 与 API 请求链路,附带 mock/live 双轨基准评测脚本、公开测试语料及使用/效果文档。

Changes

增强图抽取功能

Layer / File(s) Summary
共享类型与告警/Schema 索引
.../property_graph_extract_enhanced/types.py, warnings.py, schema_index.py, __init__.py, tests/.../test_property_graph_warnings.py, test_property_graph_schema.py
新增 CandidateGraph/NormalizedChunkGraph/DocumentGraph 类型、StructuredWarning/WarningCode 告警体系及 GraphSchemaIndex 的 schema 查询与属性类型强制转换。
候选图解析器
.../candidate_parser.py, tests/.../test_property_graph_postprocess.py(解析部分)
新增 CandidateGraphParser,剥离代码围栏、定位解析 JSON、路由 vertices/edges 并生成结构化告警。
模式感知归一化
.../normalizer.py, tests/.../test_property_graph_postprocess.py(归一化部分)
新增 SchemaAwareNormalizer,执行标签校验、属性过滤/强制转换、主键校验、canonical id 与端点解析。
文档级组装
.../document_assembler.py, tests/.../test_property_graph_postprocess.py(组装部分)
新增 DocumentGraphAssembler,跨 chunk 合并顶点、修复端点、去重边并汇总告警。
提示契约/质量门控/评估器
.../prompt_contract.py, quality_gate.py, evaluator.py, tests/.../test_property_graph_quality_gate.py, test_property_graph_evaluator.py
新增约束提示生成、质量指标计算与预测/期望图的 precision/recall/F1 评估器。
管线集成
operators/llm_op/property_graph_extract.py, nodes/llm_node/extract_info.py, tests/.../test_property_graph_extract.py
PropertyGraphExtract 按 extract_strategy 分派到新增 _run_enhanced 管线,并传参给下游组件。
API/流程/状态透传
api/models/graph_extract_requests.py, api/graph_extract_api.py, flows/graph_extract.py, state/ai_state.py, tests/api/test_graph_extract_api.py
请求新增 extract_strategy/include_debug 字段,流程与状态透传并组装 enhanced 专属响应 meta。
Mock 基准测试
tests/operators/llm_op/test_property_graph_benchmark.py
16 个确定性场景对比 baseline/enhanced,断言 F1 相对提升阈值与场景级表现。
效果报告与用法文档
docs/quality/schema-based-graph-extract-report.md, -usage.md
说明 mock/live 评测结果、向后兼容性、结构化告警与质量指标及适用场景。
Live 基准脚本与语料
scripts/build_public_actor_corpus.py, scripts/graph_extract_live_benchmark.py, tests/data/public_actor_corpus.json, pyproject.toml
构建公开演员语料、基于 DeepSeek 的 live 基准评测脚本及 ruff 配置更新。

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GraphExtractApi
  participant GraphExtractFlow
  participant PropertyGraphExtract
  participant EnhancedPipeline

  Client->>GraphExtractApi: POST /graph/extract(extract_strategy="enhanced", include_debug)
  GraphExtractApi->>GraphExtractFlow: schedule_flow(kwargs)
  GraphExtractFlow->>PropertyGraphExtract: run(context)
  PropertyGraphExtract->>EnhancedPipeline: parse -> normalize -> assemble -> quality_gate
  EnhancedPipeline-->>PropertyGraphExtract: vertices, edges, structured_warnings, quality_metrics
  PropertyGraphExtract-->>GraphExtractFlow: context
  GraphExtractFlow-->>GraphExtractApi: payload
  GraphExtractApi-->>Client: response(meta, warnings)
Loading

Poem

兔兔挖洞找 schema,
顶点边儿排整齐,
告警提醒别乱写,🐇
跨块合并不重复,
F1 涨了心欢喜~
🥕📊✨

🚥 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 标题准确概括了本次变更的核心:为图抽取新增可选的 enhanced 策略。
Linked Issues check ✅ Passed 实现了可独立启用的 enhanced 策略、基准对比、测试覆盖、报告与文档,满足 #74 的主要要求。
Out of Scope Changes check ✅ Passed 新增内容主要围绕增强抽取、评测、文档和复现脚本,未见明显与目标无关的代码变更。
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.

@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 implements a schema-aware graph extraction quality layer as an opt-in upgrade, introducing modules for parsing, normalization, document assembly, and quality evaluation. The feedback highlights several valuable improvement opportunities: replacing discouraged assert statements with robust error handling in the normalizer, simplifying schema parsing by utilizing the GraphSchemaIndex.from_schema helper, fixing a hashability bug in the frozen StructuredWarning dataclass caused by a mutable dict context, and resolving a contradiction and redundancy in the boolean-to-string coercion logic.

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 on lines +237 to +239
endpoint_spec = self._schema.edge_endpoint_spec(label)
assert endpoint_spec is not None # is_edge_label already verified
schema_out_label, schema_in_label = endpoint_spec

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using assert for data validation or control flow is discouraged because assertions can be disabled in production when Python is run with optimization flags (e.g., python -O). If endpoint_spec is None (which can happen if the schema is malformed or incomplete, e.g., missing source_label or target_label), this will raise a TypeError when unpacking. It is safer and more robust to handle this case gracefully by appending a warning and returning None, warnings.

        endpoint_spec = self._schema.edge_endpoint_spec(label)
        if endpoint_spec is None:
            warnings.append(
                StructuredWarning(
                    code=WarningCode.EDGE_LABEL_NOT_IN_SCHEMA,
                    item_type="edge",
                    reason=f"edge label {label!r} has invalid or missing source/target labels in schema",
                    label=label,
                    chunk_id=chunk_id,
                )
            )
            return None, warnings
        schema_out_label, schema_in_label = endpoint_spec

Comment on lines +166 to +167
schema_dict = schema if isinstance(schema, Mapping) else json.loads(schema)
schema_index = GraphSchemaIndex(schema_dict)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Instead of manually parsing the schema string or dict, you can leverage the existing GraphSchemaIndex.from_schema classmethod. This simplifies the code and ensures consistent validation (such as checking if the schema string starts with { and raising a helpful ValueError instead of a raw JSONDecodeError).

Suggested change
schema_dict = schema if isinstance(schema, Mapping) else json.loads(schema)
schema_index = GraphSchemaIndex(schema_dict)
schema_index = GraphSchemaIndex.from_schema(schema)

Comment on lines +146 to +149
if self.context is not None:
# Freeze the context so the dataclass stays truly immutable and
# dedup/hashing works via to_hashable_tuple below.
object.__setattr__(self, "context", dict(self.context))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In Python, dict is mutable and unhashable. Because StructuredWarning is a frozen dataclass, any attempt to hash an instance that has a non-None context (e.g., placing it in a set or using it as a dict key) will raise TypeError: unhashable type: 'dict'. This breaks the docstring claim that StructuredWarning is 'cheap to hash and deduplicate'. Additionally, the comment refers to a non-existent to_hashable_tuple method. To make the class truly hashable, you should either convert the context dict to a frozenset of items, or implement custom __hash__ and __eq__ methods that handle the dict context safely.

Comment on lines +246 to +249
if isinstance(value, bool):
# str(True) → "True" is rarely what an integrator wants; keep it
# explicit rather than silently converting.
return str(value), None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The comment says 'str(True) -> "True" is rarely what an integrator wants; keep it explicit rather than silently converting', but the code then proceeds to silently convert it anyway by returning str(value), None. Furthermore, if this branch is removed, the subsequent try: return str(value), None block will handle the boolean value in exactly the same way. If you want to allow boolean-to-string conversion, this branch is redundant and the comment is confusing. If you want to reject it, you should return a coercion failure instead.

@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: 3

🧹 Nitpick comments (9)
hugegraph-llm/src/tests/operators/llm_op/test_property_graph_warnings.py (1)

140-145: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

测试有意避开了 context 非空场景下的 hash() 校验

当前测试仅覆盖 context=None 时的可哈希性,注释也承认了这一限制。建议在修复 warnings.py 中 context 字段可哈希性问题后,补充一个带 contextStructuredWarning 哈希测试用例,以防止回归。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/operators/llm_op/test_property_graph_warnings.py`
around lines 140 - 145, The current hashability test only covers
StructuredWarning instances with context=None and misses the failing
context-bearing case. Update the warnings.py fix by adding a new test alongside
test_hashable_and_dedupable that creates a StructuredWarning with a non-empty
context and verifies hash() and set deduplication still work, using the same
StructuredWarning and WarningCode symbols so the regression is covered.

Source: Path instructions

hugegraph-llm/src/tests/operators/llm_op/test_property_graph_schema.py (1)

317-336: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

建议补充非法日历日期的测试用例

结合 schema_index.py 中 DATE 校验仅验证格式的问题,建议在此处补充如 "2024-13-40"(月份非法)的测试用例,覆盖修复后的行为。

🤖 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/operators/llm_op/test_property_graph_schema.py`
around lines 317 - 336, The date coercion tests in TestCoerceDate should also
cover invalid calendar dates, not just invalid formats. Add a case around
GraphSchemaIndex.coerce_property_value for a string like "2024-13-40" and assert
it is rejected with a non-null reason, alongside the existing passthrough and
format-rejection tests. This will ensure the DATE validation in schema_index.py
is exercised for malformed but format-like inputs.
hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/schema_index.py (1)

297-300: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

DATE 校验仅验证格式,未验证日历有效性

正则 _DATE_RE 只检查 YYYY-MM-DD 形状,像 "2024-13-40" 这种非法日期也会被当作合法值通过。建议用 datetime.strptime 做严格校验,避免非法日期流入下游图数据。

♻️ 建议修复
+from datetime import datetime
+
 ...
         if data_type == "DATE":
-            if isinstance(value, str) and _DATE_RE.match(value.strip()):
-                return value.strip(), None
-            return None, f"property '{key}' value {value!r} is not a YYYY-MM-DD DATE"
+            if isinstance(value, str):
+                candidate = value.strip()
+                if _DATE_RE.match(candidate):
+                    try:
+                        datetime.strptime(candidate, "%Y-%m-%d")
+                        return candidate, None
+                    except ValueError:
+                        pass
+            return None, f"property '{key}' value {value!r} is not a YYYY-MM-DD DATE"
🤖 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/property_graph_extract_enhanced/schema_index.py`
around lines 297 - 300, The DATE handling in the value validation path only
checks the string shape via _DATE_RE, so invalid calendar dates can slip
through. Update the DATE branch in the property validation logic that returns
the normalized value and error to use a strict datetime.strptime-based parse
after trimming the input, and only accept the value when parsing succeeds. Keep
the existing key/value error reporting in the same validation method so
malformed dates like 2024-13-40 are rejected.
hugegraph-llm/src/tests/operators/llm_op/test_property_graph_quality_gate.py (1)

196-221: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

建议补充跨 chunk 重复实体场景下 property_valid_ratio 的用例。

现有 TestPropertyValidRatio 用例(test_full_valid/test_half_dropped/test_property_conflict_is_not_counted_as_invalid)均为单 chunk(无合并)场景,未覆盖"重复顶点/边合并 + 非法属性跨多个 chunk 出现"的组合情形,而这正是 quality_gate.pykept_properties(合并后)与 property_drops(合并前累计)基准不一致可能暴露问题的场景。建议补充一个多 chunk 合并的用例以验证该指标行为符合预期。

As per path instructions, "For pipeline changes, tests should cover the relevant flow, node, or operator contract instead of only testing a helper in isolation."

🤖 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/operators/llm_op/test_property_graph_quality_gate.py`
around lines 196 - 221, `TestPropertyValidRatio` only covers single-chunk
`GraphQualityGate.compute` cases and misses the merged-entity flow where
repeated vertices/edges span multiple chunks. Add a test in
`test_property_graph_quality_gate.py` that exercises
`DocumentGraph`/quality-gate behavior with duplicate entities across chunks plus
invalid properties, and assert `property_valid_ratio` matches the post-merge
contract rather than only isolated helper counting. Use the existing
`GraphQualityGate.compute`, `DocumentGraph`, and `_w(WarningCode...)` patterns
to locate and extend the coverage.

Source: Path instructions

hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/prompt_contract.py (1)

45-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

建议为 build_prompt_contract 增加直接单测。

该函数目前仅通过 property_graph_extract.py 的端到端测试间接覆盖(见上下文片段 property_graph_extract.py:166-238)。鉴于其输出内容直接影响 LLM 抽取质量(label 白名单、主键约束等文本),建议补充针对 schema_index 为空 schema(vertex_label_names()/edge_label_names() 均为空,应输出 "(none)")等边界场景的直接单测,以锁定契约文本的稳定性。

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/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/prompt_contract.py`
around lines 45 - 70, Add direct unit coverage for build_prompt_contract because
the prompt text is only indirectly exercised today. Create tests for
build_prompt_contract in prompt_contract.py that verify the contract content for
an empty GraphSchemaIndex (vertex_label_names() and edge_label_names() both
empty should render "(none)") and a non-empty schema, and assert the stable
inclusion of the label whitelist, primary-key requirement, and JSON-only
instruction. Keep the test focused on build_prompt_contract and GraphSchemaIndex
so future prompt text changes are intentional and covered.

Source: Path instructions

hugegraph-llm/src/tests/operators/llm_op/test_property_graph_benchmark.py (1)

814-839: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

设计阈值的相对增益计算逻辑重复

Line 817 与 Line 834 各自独立计算了一次 (avg_enhanced - avg_baseline) / avg_baseline,且零值保护策略略有差异(前者失败时回退为 0.0 仅用于展示,后者用 if avg_baseline > 0 直接跳过断言)。可提取为单一辅助函数复用,减少后续修改时两处不一致的风险。鉴于当前 mock 场景下 avg_baseline 恒为正值,实际收益有限,可择机处理。

🤖 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/operators/llm_op/test_property_graph_benchmark.py`
around lines 814 - 839, The relative gain calculation is duplicated in the
benchmark test logic, with slightly different zero-handling between the table
output and the threshold assertion. Refactor the repeated `(avg_enhanced -
avg_baseline) / avg_baseline` logic in `test_property_graph_benchmark.py` into a
single helper used by the F1 summary rows and the `DESIGN_F1_RELATIVE_GAIN_MIN`
assertion, so `avg_baseline` zero handling stays consistent and future changes
only happen in one place.
docs/quality/schema-based-graph-extract-report.md (1)

116-124: 🧹 Nitpick | 🔵 Trivial

48-run 完整存档未随 PR 提交,建议以工件形式托管而非"按需索取"

文档已充分说明未提交完整存档的原因(体积约 600KB/2.1 万行,占 diff 的 ~67%),并提供了 sha256 与 jq 复核脚本,整体是合理的取舍。但"Available on request"依赖人工传递,长期看不利于可复现性与审计。建议后续将该存档上传至 CI artifacts、Git LFS 或对象存储并在文档中给出稳定链接,而不是仅承诺"可索取"。

Also applies to: 153-177

🤖 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 `@docs/quality/schema-based-graph-extract-report.md` around lines 116 - 124,
The live-benchmark archive is currently described as “Available on request,”
which leaves the 48-run record hard to access and audit. Update the
documentation in the artifacts table and related cross-checking section to point
to a stable hosted artifact location (for example CI artifacts, Git LFS, or
object storage) instead of manual request flow, using the existing
live-benchmark archive and cross-checking references to keep the wording
consistent.
scripts/graph_extract_live_benchmark.py (2)

297-309: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

手写 .env.local 解析器可用项目已有依赖 python-dotenv 替代

_load_env_local 手动逐行 partition("=") 解析,未处理引号包裹的值、行内注释、export 前缀等常见 .env 语法。pyproject.toml 中已声明 python-dotenv~=1.0.1 依赖,建议直接使用 dotenv.load_dotenv(env_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 `@scripts/graph_extract_live_benchmark.py` around lines 297 - 309, The
_load_env_local helper is manually parsing .env.local and missing common dotenv
features like quoted values, inline comments, and export prefixes. Replace the
custom line-by-line parsing in _load_env_local with python-dotenv’s load_dotenv
using env_path so the existing dependency handles .env syntax correctly and the
rest of graph_extract_live_benchmark.py can keep using os.environ.setdefault
behavior.

341-402: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

主循环中途失败会丢失已产生费用的运行结果,且无重试机制

main() 中对 corpora × strategy × runs(最多 48 次)的循环(Line 379-402)没有任何异常捕获;若第 30 次调用因瞬时网络错误或 DeepSeek 端限流而抛出异常,前面已经花费真实费用产生的 47 次结果都不会被写入 --output(落盘写入在 Line 468-469 才发生,循环结束后一次性完成)。建议:

  1. 为单次 LLM 调用增加重试(项目已依赖 tenacity~=8.5.0,可直接复用);
  2. 或在循环内增量写入 all_runs 到磁盘(每完成一个 run 追加一条记录),避免因中途失败而必须整轮重跑并重新付费。
🤖 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 `@scripts/graph_extract_live_benchmark.py` around lines 341 - 402, The main
loop in main() can lose all previously completed run results if a later
_run_strategy_on_corpus call raises, and it has no retry/backoff for transient
API failures. Add tenacity-based retries around the single LLM execution path in
_run_strategy_on_corpus or the call site in main(), and make run progress
durable by incrementally writing each completed SingleRunResult to the --output
archive instead of waiting until the end. Use the existing symbols main,
_run_strategy_on_corpus, and all_runs to locate the fix.
🤖 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/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/normalizer.py`:
- Around line 334-345: The legacy source/target normalization in normalizer.py
and the edge assembly path in document_assembler.py can produce canonical
outV/inV ids without verifying the endpoints exist in the final vertices set,
which can lead to dangling edges. Update the assembly flow around the relevant
normalization and edge-building logic to check each edge endpoint against the
final vertex collection before emitting it; if either endpoint is missing, drop
the edge and log a warning. Also add a safeguard in
Commit2Graph.load_into_graph() so addEdge() is only called for edges whose
vertices are already present, using the existing vertex/edge assembly symbols to
keep the fix localized.

In
`@hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/warnings.py`:
- Around line 26-29: `StructuredWarning` currently becomes unhashable when
`context` is non-empty because the frozen dataclass includes the mutable `dict`
in its default hash. Update `StructuredWarning` in `warnings.py` so hashing and
deduplication do not depend on `context`—for example by excluding `context` from
equality/hash generation or converting it to a stable, hashable representation
inside `__post_init__`. Keep the `WarningCode` behavior unchanged and ensure
`StructuredWarning` remains safe to use in sets and as dict keys.

In `@hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py`:
- Around line 150-232: The _run_enhanced path is overwriting any pre-existing
context["vertices"] and context["edges"] instead of preserving and merging them,
which makes the enhanced strategy inconsistent with the baseline behavior.
Update the final context assignment in _run_enhanced to append or merge
doc_graph.vertices and doc_graph.edges with any values already present in
context, using the existing _run_enhanced and DocumentGraphAssembler flow as the
place to fix it. Also add a test covering a pre-populated context passed through
the enhanced strategy to verify the original vertices/edges are retained.

---

Nitpick comments:
In `@docs/quality/schema-based-graph-extract-report.md`:
- Around line 116-124: The live-benchmark archive is currently described as
“Available on request,” which leaves the 48-run record hard to access and audit.
Update the documentation in the artifacts table and related cross-checking
section to point to a stable hosted artifact location (for example CI artifacts,
Git LFS, or object storage) instead of manual request flow, using the existing
live-benchmark archive and cross-checking references to keep the wording
consistent.

In
`@hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/prompt_contract.py`:
- Around line 45-70: Add direct unit coverage for build_prompt_contract because
the prompt text is only indirectly exercised today. Create tests for
build_prompt_contract in prompt_contract.py that verify the contract content for
an empty GraphSchemaIndex (vertex_label_names() and edge_label_names() both
empty should render "(none)") and a non-empty schema, and assert the stable
inclusion of the label whitelist, primary-key requirement, and JSON-only
instruction. Keep the test focused on build_prompt_contract and GraphSchemaIndex
so future prompt text changes are intentional and covered.

In
`@hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/schema_index.py`:
- Around line 297-300: The DATE handling in the value validation path only
checks the string shape via _DATE_RE, so invalid calendar dates can slip
through. Update the DATE branch in the property validation logic that returns
the normalized value and error to use a strict datetime.strptime-based parse
after trimming the input, and only accept the value when parsing succeeds. Keep
the existing key/value error reporting in the same validation method so
malformed dates like 2024-13-40 are rejected.

In `@hugegraph-llm/src/tests/operators/llm_op/test_property_graph_benchmark.py`:
- Around line 814-839: The relative gain calculation is duplicated in the
benchmark test logic, with slightly different zero-handling between the table
output and the threshold assertion. Refactor the repeated `(avg_enhanced -
avg_baseline) / avg_baseline` logic in `test_property_graph_benchmark.py` into a
single helper used by the F1 summary rows and the `DESIGN_F1_RELATIVE_GAIN_MIN`
assertion, so `avg_baseline` zero handling stays consistent and future changes
only happen in one place.

In
`@hugegraph-llm/src/tests/operators/llm_op/test_property_graph_quality_gate.py`:
- Around line 196-221: `TestPropertyValidRatio` only covers single-chunk
`GraphQualityGate.compute` cases and misses the merged-entity flow where
repeated vertices/edges span multiple chunks. Add a test in
`test_property_graph_quality_gate.py` that exercises
`DocumentGraph`/quality-gate behavior with duplicate entities across chunks plus
invalid properties, and assert `property_valid_ratio` matches the post-merge
contract rather than only isolated helper counting. Use the existing
`GraphQualityGate.compute`, `DocumentGraph`, and `_w(WarningCode...)` patterns
to locate and extend the coverage.

In `@hugegraph-llm/src/tests/operators/llm_op/test_property_graph_schema.py`:
- Around line 317-336: The date coercion tests in TestCoerceDate should also
cover invalid calendar dates, not just invalid formats. Add a case around
GraphSchemaIndex.coerce_property_value for a string like "2024-13-40" and assert
it is rejected with a non-null reason, alongside the existing passthrough and
format-rejection tests. This will ensure the DATE validation in schema_index.py
is exercised for malformed but format-like inputs.

In `@hugegraph-llm/src/tests/operators/llm_op/test_property_graph_warnings.py`:
- Around line 140-145: The current hashability test only covers
StructuredWarning instances with context=None and misses the failing
context-bearing case. Update the warnings.py fix by adding a new test alongside
test_hashable_and_dedupable that creates a StructuredWarning with a non-empty
context and verifies hash() and set deduplication still work, using the same
StructuredWarning and WarningCode symbols so the regression is covered.

In `@scripts/graph_extract_live_benchmark.py`:
- Around line 297-309: The _load_env_local helper is manually parsing .env.local
and missing common dotenv features like quoted values, inline comments, and
export prefixes. Replace the custom line-by-line parsing in _load_env_local with
python-dotenv’s load_dotenv using env_path so the existing dependency handles
.env syntax correctly and the rest of graph_extract_live_benchmark.py can keep
using os.environ.setdefault behavior.
- Around line 341-402: The main loop in main() can lose all previously completed
run results if a later _run_strategy_on_corpus call raises, and it has no
retry/backoff for transient API failures. Add tenacity-based retries around the
single LLM execution path in _run_strategy_on_corpus or the call site in main(),
and make run progress durable by incrementally writing each completed
SingleRunResult to the --output archive instead of waiting until the end. Use
the existing symbols main, _run_strategy_on_corpus, and all_runs to locate the
fix.
🪄 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: 9cae20d0-42af-43e0-b30e-9171047f30eb

📥 Commits

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

📒 Files selected for processing (30)
  • docs/quality/schema-based-graph-extract-report.md
  • docs/quality/schema-based-graph-extract-usage.md
  • hugegraph-llm/src/hugegraph_llm/api/graph_extract_api.py
  • hugegraph-llm/src/hugegraph_llm/api/models/graph_extract_requests.py
  • hugegraph-llm/src/hugegraph_llm/flows/graph_extract.py
  • hugegraph-llm/src/hugegraph_llm/nodes/llm_node/extract_info.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/__init__.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/candidate_parser.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/document_assembler.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/evaluator.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/normalizer.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/prompt_contract.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/quality_gate.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/schema_index.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/types.py
  • hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/warnings.py
  • hugegraph-llm/src/hugegraph_llm/state/ai_state.py
  • hugegraph-llm/src/tests/api/test_graph_extract_api.py
  • hugegraph-llm/src/tests/data/public_actor_corpus.json
  • hugegraph-llm/src/tests/operators/llm_op/test_property_graph_benchmark.py
  • hugegraph-llm/src/tests/operators/llm_op/test_property_graph_evaluator.py
  • hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
  • hugegraph-llm/src/tests/operators/llm_op/test_property_graph_postprocess.py
  • hugegraph-llm/src/tests/operators/llm_op/test_property_graph_quality_gate.py
  • hugegraph-llm/src/tests/operators/llm_op/test_property_graph_schema.py
  • hugegraph-llm/src/tests/operators/llm_op/test_property_graph_warnings.py
  • pyproject.toml
  • scripts/build_public_actor_corpus.py
  • scripts/graph_extract_live_benchmark.py

Comment on lines +334 to +345
# Tier 1 & 2 combined: legacy source/target dict has enough for the
# schema-only canonical id, which is stateless w.r.t. the chunk.
if isinstance(legacy, Mapping):
legacy_label = legacy.get("label")
legacy_props = legacy.get("properties")
if isinstance(legacy_label, str) and isinstance(legacy_props, Mapping):
canonical = self._schema.canonical_vertex_id(legacy_label, legacy_props)
if canonical is not None:
return canonical, None
# No canonical possible — keep the legacy dict as a hint so
# the assembler can decide.
return None, {"legacy": {"label": legacy_label, "properties": dict(legacy_props)}}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 查看下游如何提交顶点/边,确认边端点是否需要顶点先存在或会被隐式创建
fd -e py commit | head
rg -nP -C3 '\b(add_vertices|addVertices|add_edges|addEdges|commit(_|)to|G\.merge|batch)' --type=py -g '!**/tests/**' | head -80
# 组装器是否对边端点做存在性校验
rg -nP -C3 '(outV|inV).*(in |not in ).*(vertice|merged|id)' --type=py hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/ | head

Repository: hugegraph/hugegraph-ai

Length of output: 480


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) 先看提交到 HugeGraph 的实现,确认边是否依赖已存在顶点
ast-grep outline hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py
ast-grep outline hugegraph-llm/src/hugegraph_llm/nodes/hugegraph_node/commit_to_hugegraph.py

# 2) 读取关键片段
wc -l hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py
wc -l hugegraph-llm/src/hugegraph_llm/nodes/hugegraph_node/commit_to_hugegraph.py
sed -n '1,260p' hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py
sed -n '1,260p' hugegraph-llm/src/hugegraph_llm/nodes/hugegraph_node/commit_to_hugegraph.py

# 3) 定位文档组装器和 endpoint 修复/去重逻辑
rg -n "def _resolve_pending|def _repair_endpoints|def _dedupe_edges|DocumentGraph|legacy" hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/ -n
sed -n '240,380p' hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/document_assembler.py
sed -n '300,380p' hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/normalizer.py

# 4) 找到测试里是否覆盖“端点不存在/边先于点”的行为
rg -n "dangling|orphan|outV|inV|vertex.*edge|edge.*vertex|commit_to_hugegraph|DocumentGraph" hugegraph-llm/src/tests -n

Repository: hugegraph/hugegraph-ai

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) 只读取组装器相关的关键区间,避免输出过大
sed -n '180,340p' hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/document_assembler.py
sed -n '220,360p' hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/normalizer.py

# 2) 只看测试里与 dangling / endpoint repair 直接相关的断言
sed -n '900,1095p' hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
sed -n '1,340p' hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph_load_into_graph.py

Repository: hugegraph/hugegraph-ai

Length of output: 33549


补上端点存在性校验,避免写入悬挂边。 normalizer.py:334-345 / document_assembler.py:299-309 会把 legacy source/target 解析成 canonical outV/inV,但不会确认对应顶点已进入最终 verticesCommit2Graph.load_into_graph() 也只是先写顶点再直接 addEdge(),没有兜底创建缺失端点。这样当顶点在归一化阶段被丢弃或只在边里出现时,最终会把边写成指向不存在顶点的 id。建议在组装阶段按最终顶点集合校验端点,不存在时直接丢弃并告警。

🤖 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/property_graph_extract_enhanced/normalizer.py`
around lines 334 - 345, The legacy source/target normalization in normalizer.py
and the edge assembly path in document_assembler.py can produce canonical
outV/inV ids without verifying the endpoints exist in the final vertices set,
which can lead to dangling edges. Update the assembly flow around the relevant
normalization and edge-building logic to check each edge endpoint against the
final vertex collection before emitting it; if either endpoint is missing, drop
the edge and log a warning. Also add a safeguard in
Commit2Graph.load_into_graph() so addEdge() is only called for edges whose
vertices are already present, using the existing vertex/edge assembly symbols to
keep the fix localized.

Comment on lines +26 to +29
A ``WarningCode`` is a ``str, Enum`` so it JSON-serializes as its bare code name
(``"ENDPOINT_UNRESOLVED"``) without needing custom encoders. ``StructuredWarning``
is a frozen dataclass, cheap to hash and deduplicate.
"""

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
rg -n 'hash\(|set\(|frozenset\(' hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/document_assembler.py hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/quality_gate.py 2>/dev/null

Repository: hugegraph/hugegraph-ai

Length of output: 937


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first
ast-grep outline hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/warnings.py --view expanded

# Read the relevant sections with line numbers
cat -n hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/warnings.py | sed -n '1,260p'

# Inspect the associated test file
fd -a 'test_property_graph_warnings.py' hugegraph-llm
cat -n "$(fd -a 'test_property_graph_warnings.py' hugegraph-llm | head -n 1)" | sed -n '1,220p'

# Check the warning call sites that pass context
rg -n 'StructuredWarning\(' hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced

Repository: hugegraph/hugegraph-ai

Length of output: 24827


StructuredWarningcontext 时会失去可哈希性

context__post_init__ 里被转成普通 dict,而这个 frozen dataclass 会把所有字段纳入默认 __hash__。因此只要 context 非空,hash(warning) / set 去重就会抛 TypeError,和“cheap to hash and deduplicate”的说明不一致。把 context 从哈希里排除,或改成可哈希表示。

建议修复
-    context: Optional[Mapping[str, Any]] = field(default=None)
+    context: Optional[Mapping[str, Any]] = field(default=None, hash=False)
🤖 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/property_graph_extract_enhanced/warnings.py`
around lines 26 - 29, `StructuredWarning` currently becomes unhashable when
`context` is non-empty because the frozen dataclass includes the mutable `dict`
in its default hash. Update `StructuredWarning` in `warnings.py` so hashing and
deduplication do not depend on `context`—for example by excluding `context` from
equality/hash generation or converting it to a stable, hashable representation
inside `__post_init__`. Keep the `WarningCode` behavior unchanged and ensure
`StructuredWarning` remains safe to use in sets and as dict keys.

Comment on lines +150 to +232
# ------------------------------------------------------- enhanced strategy
def _run_enhanced(
self,
context: Dict[str, Any],
schema: Any,
chunks: List[str],
) -> Dict[str, List[Any]]:
"""Enhanced extraction: LLM → parser → normalizer → assembler → quality gate.

The schema is threaded through the pipeline twice: once as text embedded
in the LLM prompt (via ``generate_extract_property_graph_prompt``), and
once as a compiled ``GraphSchemaIndex`` used by every post-LLM stage.
Warnings from every stage accumulate into a single list surfaced via
``context["structured_warnings"]``; the assembled document graph plus
the quality-gate aggregate populate the remaining meta fields.
"""
schema_dict = schema if isinstance(schema, Mapping) else json.loads(schema)
schema_index = GraphSchemaIndex(schema_dict)

parser = CandidateGraphParser()
normalizer = SchemaAwareNormalizer(schema_index)
assembler = DocumentGraphAssembler(schema_index)
constraint_block = build_prompt_contract(schema_index)

warnings = []
chunk_graphs = []
candidate_vertex_total = 0
candidate_edge_total = 0
debug_records: List[Dict[str, Any]] = []

for chunk_id, chunk in enumerate(chunks):
raw = self._extract_property_graph_by_llm_enhanced(schema, chunk, constraint_block)
log.debug(
"[LLM enhanced] %s chunk_id=%s output: %s",
self.__class__.__name__,
chunk_id,
raw,
)
candidate, parse_warnings = parser.parse(raw, chunk_id=chunk_id)
warnings.extend(parse_warnings)
candidate_vertex_total += len(candidate.vertices)
candidate_edge_total += len(candidate.edges)

normalized, norm_warnings = normalizer.normalize(candidate, chunk_id=chunk_id)
warnings.extend(norm_warnings)
chunk_graphs.append(normalized)

if self.include_debug:
debug_records.append(
{
"chunk_id": chunk_id,
"raw_output": _truncate(raw, _DEBUG_RAW_OUTPUT_LIMIT),
"candidate_vertex_count": len(candidate.vertices),
"candidate_edge_count": len(candidate.edges),
"normalized_vertex_count": len(normalized.vertices),
"normalized_edge_count": len(normalized.edges),
"chunk_warning_count": len(parse_warnings) + len(norm_warnings),
}
)

doc_graph, asm_warnings = assembler.assemble(chunk_graphs)
warnings.extend(asm_warnings)

metrics = GraphQualityGate.compute(
doc_graph,
warnings=warnings,
candidate_vertex_count=candidate_vertex_total,
candidate_edge_count=candidate_edge_total,
)

context["vertices"] = doc_graph.vertices
context["edges"] = doc_graph.edges
context["call_count"] = context.get("call_count", 0) + len(chunks)
context["extract_strategy"] = "enhanced"
context["chunk_count"] = len(chunks)
context["structured_warnings"] = [w.to_dict() for w in warnings]
context["quality_metrics"] = metrics.to_dict()
if self.include_debug:
context["debug_info"] = {
"chunks": debug_records,
"warning_code_distribution": metrics.warning_code_distribution,
}
return context

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== property_graph_extract.py outline ==\n'
ast-grep outline hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py --view expanded || true

printf '\n== property_graph_extract.py relevant lines ==\n'
sed -n '1,280p' hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py | cat -n

printf '\n== search tests for vertices/edges context handling ==\n'
rg -n "existing vertices|existing edges|structured_warnings|extract_strategy|_run_enhanced|vertices.*append|edges.*append|call_count" hugegraph-llm/tests hugegraph-llm/src -g '*test*.py' -g '*.py'

Repository: hugegraph/hugegraph-ai

Length of output: 39584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== enhanced/baseline tests around existing context ==\n'
sed -n '1290,1365p' hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py | cat -n

printf '\n== nearby enhanced tests ==\n'
sed -n '220,360p' hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py | cat -n

printf '\n== flow handling of extracted context ==\n'
sed -n '100,150p' hugegraph-llm/src/hugegraph_llm/flows/graph_extract.py | cat -n

printf '\n== inspect enhanced assembler contract ==\n'
sed -n '1,320p' hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/document_assembler.py | cat -n

Repository: hugegraph/hugegraph-ai

Length of output: 29748


增强策略应保留已有的 vertices/edges
hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py:220-221 这里直接用 doc_graph.vertices/edges 覆盖了 context,会把调用方预先放入的顶点/边静默丢掉;baseline 分支是追加,语义不一致。应改为合并,并补一个“预填充 context + enhanced”用例。

🤖 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/property_graph_extract.py`
around lines 150 - 232, The _run_enhanced path is overwriting any pre-existing
context["vertices"] and context["edges"] instead of preserving and merging them,
which makes the enhanced strategy inconsistent with the baseline behavior.
Update the final context assignment in _run_enhanced to append or merge
doc_graph.vertices and doc_graph.edges with any values already present in
context, using the existing _run_enhanced and DocumentGraphAssembler flow as the
place to fix it. Also add a test covering a pre-populated context passed through
the enhanced strategy to verify the original vertices/edges are retained.

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] 效果优先的 Schema-based 图抽取功能实现

1 participant