Files
semantica/tests/kg
1ec8de5d25 fix(kg): promote synthetic relation endpoints into the graph (#1464)
* fix(kg): promote synthetic relation endpoints into the graph

Relation extraction synthesizes an UNKNOWN Entity for a relationship endpoint
that is absent from the NER entity list (metadata synthetic=True), but
GraphBuilder kept only the endpoint string, so the synthetic object never
reached the entity collection. GraphValidator then reported DANGLING_EDGE,
breaking the native NER -> relation -> graph -> validation pipeline.

The relation branch now collects synthetic endpoint entities and promotes them
into the entity collection (deduplicated by id, keeping confidence=0.8 and the
synthetic metadata). A new unknown_relation_endpoint option defaults to
include; set it to "reject" to drop relationships whose endpoints are
synthetic instead.

Closes #1463

* fix(kg): reconcile synthetic endpoints after resolution

Address Qodo review feedback on #1464:

- Read unknown_relation_endpoint through the nested "config" mapping the
  orchestrator passes (GraphBuilder(config=...)), so the policy actually takes
  effect end to end.
- Prefer a real entity over an earlier-promoted synthetic one with the same id,
  deduplicating at build tail (unhashable ids are skipped defensively).
- Tag LLM triple endpoints that match no extracted entity and promote them too,
  so triplet relationships no longer leave dangling edges.
- Log when the reject policy drops a relationship.

Closes #1463

* fix(kg): key dedup on both id and entity_id; tag HF triple endpoints

Follow-up hardening on #1464's synthetic-endpoint reconcile:

- Dedup promoted endpoints against both `id` and `entity_id` (the canonical
  keys the builder itself recognizes), in both the relation branch and the
  build-tail reconciliation. The build-tail pass now also skips unhashable
  ids defensively.
- Tag HuggingFace REBEL endpoint texts that match no extracted entity, same
  as the LLM typed path, so HF triplets no longer leave dangling edges.
- Tests: use a real `Triplet`, cover the `entity_id` dedup case, exercise
  `merge_entities=True` explicitly (the prior test mislabeled the default),
  and lock in the dict-relationship out-of-scope contract.

* fix(kg): guard metadata=None and gate endpoint promotion scan

_real_ids and the reconcile filter both did .get("metadata", {}).get(...),
which returns None (not {}) when the key is present with a None value and
raised on entities carrying metadata=None.

The synthetic-endpoint promotion block in _process_item rebuilt existing_ids
by scanning all_entities for every relationship, making build() quadratic on
dense relation inputs. Gate it on if synthetic_endpoints so ordinary
relationships pay nothing (6000+6000 no-synthetic builds in 0.018s vs 9.04s).

* config(kg): home unknown_relation_endpoint in build config and document it

The option now has a default in the per-module build method config
(kg_methods.build.unknown_relation_endpoint) and kg_usage.md documents both
values. __init__ folds a config= dict into the option mapping once, which also
fixes entity_resolution/conflict_detection being silently dropped on the
orchestrator path; every option reads the same way now.

* fix(extract): thread entities into HF REBEL triplet path

extract_triplets_huggingface read entities from kwargs.get("entities") which
is always empty on this path, making the synthetic-endpoint tag a no-op
comparison. Declare entities as a parameter so it matches the real NER list
TripletExtractor already forwards.

* fix(kg): finalize synthetic relation endpoint handling

- Add concrete Python API (kg_config.set_method_config) and YAML config
  file example to kg_usage.md; the previous text referenced only an opaque
  config-file key path with no runnable code.

- Add three targeted tests to test_dangling_synthetic_endpoint.py:
  * test_real_entity_with_entity_id_only_wins_over_prior_synthetic: covers
    the _real_ids dedup pass when the real entity carries its id under
    entity_id only (no 'id' field).
  * test_llm_relation_synthetic_endpoints_promoted: exercises the full
    _parse_relation_result → Relation → GraphBuilder promotion path end-to-end.
  * test_reject_policy_emits_warning_when_all_rels_dropped: verifies that
    the 'all relationships were dropped' warning fires correctly when the
    reject policy drops every relationship from a dict-style source.

- Add import pytest (required for caplog fixture used in the new test).
- Fix missing EOF newline in the test file.

* fix(kg): make reject-policy edge drops observable

The reject policy logged at INFO and returned no machine-readable count,
making it effectively silent at WARNING-level logging thresholds.

Issue #1463 requires that rejected relationships are 'reported explicitly'.

Changes:
- Elevate the per-rejection log from INFO to WARNING so it is visible
  without debug logging enabled.
- Add self._rejected_relationships counter (reset per build() call,
  incremented per dropped edge).
- Expose the count in graph['metadata']['rejected_relationships'] so
  callers can check the outcome programmatically without log parsing.
- Document both observability mechanisms in kg_usage.md.
- Add four focused regression tests:
  * rejected count matches number of dropped edges
  * include policy always yields count=0
  * WARNING log is emitted once per dropped edge (not just INFO)
  * counter resets between successive build() calls on the same builder

---------

Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
Co-authored-by: Sameer Kadam <sameerkadam@Mac-11.lan>
2026-09-07 00:39:03 +05:30
..