Conversation
…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
|
@codecov-ai-reviewer review |
Walkthrough本 PR 新增可选启用(opt-in)的 schema-aware 增强图抽取策略,包含候选解析、模式归一化、文档级跨 chunk 组装、质量门控与离线评估器等纯 Python 组件,并将其接入 PropertyGraphExtract、GraphExtractFlow 与 API 请求链路,附带 mock/live 双轨基准评测脚本、公开测试语料及使用/效果文档。 Changes增强图抽取功能
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)
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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| schema_dict = schema if isinstance(schema, Mapping) else json.loads(schema) | ||
| schema_index = GraphSchemaIndex(schema_dict) |
There was a problem hiding this comment.
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).
| schema_dict = schema if isinstance(schema, Mapping) else json.loads(schema) | |
| schema_index = GraphSchemaIndex(schema_dict) | |
| schema_index = GraphSchemaIndex.from_schema(schema) |
| 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)) |
There was a problem hiding this comment.
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.
| if isinstance(value, bool): | ||
| # str(True) → "True" is rarely what an integrator wants; keep it | ||
| # explicit rather than silently converting. | ||
| return str(value), None |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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字段可哈希性问题后,补充一个带context的StructuredWarning哈希测试用例,以防止回归。As per path instructions, "Any code change inhugegraph-llmmust 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 winDATE 校验仅验证格式,未验证日历有效性
正则
_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.py中kept_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-llmmust 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 | 🔵 Trivial48-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 才发生,循环结束后一次性完成)。建议:
- 为单次 LLM 调用增加重试(项目已依赖
tenacity~=8.5.0,可直接复用);- 或在循环内增量写入
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
📒 Files selected for processing (30)
docs/quality/schema-based-graph-extract-report.mddocs/quality/schema-based-graph-extract-usage.mdhugegraph-llm/src/hugegraph_llm/api/graph_extract_api.pyhugegraph-llm/src/hugegraph_llm/api/models/graph_extract_requests.pyhugegraph-llm/src/hugegraph_llm/flows/graph_extract.pyhugegraph-llm/src/hugegraph_llm/nodes/llm_node/extract_info.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/__init__.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/candidate_parser.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/document_assembler.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/evaluator.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/normalizer.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/prompt_contract.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/quality_gate.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/schema_index.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/types.pyhugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract_enhanced/warnings.pyhugegraph-llm/src/hugegraph_llm/state/ai_state.pyhugegraph-llm/src/tests/api/test_graph_extract_api.pyhugegraph-llm/src/tests/data/public_actor_corpus.jsonhugegraph-llm/src/tests/operators/llm_op/test_property_graph_benchmark.pyhugegraph-llm/src/tests/operators/llm_op/test_property_graph_evaluator.pyhugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.pyhugegraph-llm/src/tests/operators/llm_op/test_property_graph_postprocess.pyhugegraph-llm/src/tests/operators/llm_op/test_property_graph_quality_gate.pyhugegraph-llm/src/tests/operators/llm_op/test_property_graph_schema.pyhugegraph-llm/src/tests/operators/llm_op/test_property_graph_warnings.pypyproject.tomlscripts/build_public_actor_corpus.pyscripts/graph_extract_live_benchmark.py
| # 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)}} |
There was a problem hiding this comment.
🗄️ 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/ | headRepository: 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 -nRepository: 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.pyRepository: hugegraph/hugegraph-ai
Length of output: 33549
补上端点存在性校验,避免写入悬挂边。 normalizer.py:334-345 / document_assembler.py:299-309 会把 legacy source/target 解析成 canonical outV/inV,但不会确认对应顶点已进入最终 vertices;Commit2Graph.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.
| 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. | ||
| """ |
There was a problem hiding this comment.
🎯 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/nullRepository: 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_enhancedRepository: hugegraph/hugegraph-ai
Length of output: 24827
StructuredWarning 带 context 时会失去可哈希性
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.
| # ------------------------------------------------------- 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 |
There was a problem hiding this comment.
🗄️ 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 -nRepository: 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.
Summary
Adds an opt-in enhanced strategy for
/graph/extractthat appliesa 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.
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):
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-onlymetakeys that appear only when the caller opts in. Baselineresponses are byte-identical to the pre-PR shape.
How to Enable
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 5LLM-output formats.
normalizer.py— per-chunk schema-aware normalizer (propertyfilter → coerce → PK check → canonical id).
document_assembler.py— cross-chunk alias-table union withambiguity 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 + propertyfidelity metrics).
prompt_contract.py— schema-derived constraint block thatenhanced 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 leadextracts + verifies GT against Wikidata
P161 (cast member)claimsvia 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 themean ± std, per-corpus, per-strategy JSON archive.
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-derivablefrom the archive (queries in the report's "How to Reproduce This
Report" section).
Tests (all
unit or contract, deterministic):test_property_graph_schema.pytest_property_graph_warnings.pytest_property_graph_postprocess.py(parser + normalizer + assembler)test_property_graph_quality_gate.pytest_property_graph_evaluator.pytest_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
parser 97 %, assembler 90 %, quality_gate 100 %, warnings 100 %).
-m "unit or contract"filter, well above the 34 % CI floor.baseline meta remains
{vertex_count, edge_count, text_count}.≥ baseline F1 on every scenario.
avg F1 gain ≥ +5 % relative. Actual: +11.6 %.
latency, tokens, cost) for all 8 corpora × 3 runs = 48 samples.
Scope Notes
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.
token_usagefield inmetaremains a placeholder pending anLLM adapter change to surface token metadata through
BaseLLM. Thelive benchmark script tracks tokens by calling the LiteLLM completion
API directly.
duplication doesn't get double-counted; raw counts are still exposed
in
ItemMetrics.predicted_count_rawfor downstream commit-loadmonitoring.
Summary by CodeRabbit
新功能
文档
测试