Skip to content

context-graph/unstructured2graph: typed relation model on a shared hygm ontology (map #344) - #373

Merged
antejavor merged 3 commits into
mainfrom
feat/typed-relation-model
Sep 30, 2026
Merged

antejavor merged 3 commits into
mainfrom
feat/typed-relation-model

Conversation

@antejavor

@antejavor antejavor commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #344. Closes #357. Closes #365. Closes #371.

Implements map #344 (typed relation model) with hand-written ontologies through ManualStrategy. The derived vocabulary (LlmRecommendationStrategy) stays an interface until its evidence run, #372, passes. The revised plan puts that second, then a full eval as confirmation.

What changes

hygm (new workspace package, pure Python)

unstructured2graph

  • Ontology loads through ManualStrategy. The YAML gains identity, start_labels and end_labels.
  • enforce_relation_domain_range is the post-hoc half of the one-spec, two-compilation model (Extraction-time constraints vs post-hoc validation #348). It runs under enforce_ontology and flags a mismatch, never deleting it. ADR 0004 is amended to cover relations.
  • ontology_report returns integrity counts and Does LightRAG consume domain/range too #349's coverage signal: untyped relationships versus none at all, and declared types with zero instances.
  • from_documents / Document / Segment: text stored verbatim, one Chunk per document, with turn segments. unstructured's partitioner rewrites text (it drops list bullets and folds tables), which would break turn offsets.
  • ensure_lookup adds a unique constraint and an index on Chunk.hash and <workspace>.entity_id (unstructured2graph: entity_id is MERGEd on but never indexed or constrained #357). Verified with EXPLAIN: in Memgraph a uniqueness constraint alone does not index, so every lookup was a label scan.
  • connect_chunks_to_entities is an indexed lookup instead of a cartesian product.

GLiNER2Backend, rewritten on gliner2.joint_ie

sessions-graph

eval

Follow-ups from running this on a 500-session eval batch:

  • Post-processing covers only the chunks just ingested (25d7349). Linking, label promotion, the domain/range check and ontology_report rescanned the whole workspace after every session: 9 s per session at 8,000 entities, 10 s at 16,000, on the way to about 400,000.
  • Pronouns typed Person go through the user rule too (f9d4f07). Without this, Person:'you'/'I'/'assistant' merged, under global identity, into hubs spanning up to 110 sessions.
  • All-lowercase mentions of a global type stay per chunk (same commit). Otherwise generic nouns like home or area merged across about 50 sessions.

The eval work built on this (hybrid retrieval and friends) is a separate stacked PR. The judge-scoring fix (#387) is also separate, off main.

Found on the way

  • Memgraph 3.13.1 planner bug: WHERE (a:X OR b:X) is planned as Filter (a:X), (b:X), an AND. Every edge with only one endpoint in the workspace, e.g. from (:User), silently escaped the check. The workaround is $workspace IN labels(a) OR ..., covered by a regression test. Worth reporting upstream.
  • duration.between doesn't exist in Memgraph. Date arithmetic is datetime subtraction.

Verification

  • The suites, run against a dedicated Memgraph:
    • hygm: 36 passed;
    • unstructured2graph: 131 passed, 10 skipped;
    • sessions-graph: 71 passed;
    • eval: 237 passed;
    • actions-graph and skills-graph: unchanged, passing.
  • ruff and ty are clean.
  • The real GLiNER2 e2e (not in CI, which never installs gliner2) passes: 3 tests.
  • Port check with the real model on Wire a constrained ontology and read the edges #350's 10 evidence sessions, hand vocabulary:
    • 2525 mentions, against the prototype's 2509;
    • all three answer edges land on the user's node: personal_best → 25:50, visited → Museum of Modern Art, studied → Business Administration;
    • 0 non-conformant entities and relationships;
    • 1 self-loop dropped, The read-time contract for non-conformant edges #355's count.

Not in this PR

https://claude.ai/code/session_01BczgXHGBrKsHSQUGB8P8te

…gm ontology (map #344)

Implements map #344's decisions with hand-written ontologies (ManualStrategy).
LlmRecommendationStrategy stays an interface until its evidence run (#372).

- hygm (new package): NodeType with identity (global/chunk/span), RelationType
  with start_labels/end_labels, validate_model (incl. User requires Person),
  ManualStrategy (YAML), OwlImportStrategy (rdflib, hygm[owl]).
- unstructured2graph: ontology loads through hygm; post-hoc domain/range check
  flags, never deletes (ADR 0004 amended); ontology_report for integrity and
  coverage; from_documents ingests verbatim text with turn segments; Chunk.hash
  and entity_id are unique AND indexed (a Memgraph unique constraint does not
  index).
- GLiNER2Backend rewritten on gliner2's joint path: schema compiled once and
  held (#365), candidate caps 4096 (#371), one window per turn (#352), the
  user-mention resolver (#358), per-type identity with MENTIONED_IN per chunk
  (#346), valid_at as a datetime from the source turn (#364), self-loops
  dropped (#355).
- sessions-graph: reconcile_session sends a segmented Document, synthesizes
  anon-<session_id> when a session has no user (#347, #354), and reports
  integrity counts in ReconciliationSummary.
- eval: injected turns carry the session date; GLiNER2 extracts against a
  bundled LongMemEval ontology (#361's hand vocabulary).
antejavor added a commit that referenced this pull request Sep 29, 2026
… and the #373 port check

derive_batched.py runs #353's contract as revised by #366 (4 propose batches,
one consolidate call, value-relation and no-tense rules, observe at 4096 caps)
from two disjoint seeds; read_derived.py reads them on the evidence sessions.
Both seeds recover Place and MoMA visited with no tense pairs, but both lose
25:50: value relations never fire into a value type under the permissive
observe pass, so prune drops them. port_check.py runs PR #373's GLiNER2Backend
over the same sessions (2525 mentions vs the prototype's 2509, all three
answer edges on the user's node, 0 non-conformant).
antejavor added a commit that referenced this pull request Sep 29, 2026
… typed GLiNER2 graph

eval_diagnosis.py traces each of PR #373's 100-question/5-session eval
answers through its evidence sessions (stated, extracted as an entity, on
an edge, on a user edge) and scores two oracle retrievals with the eval's own
answer prompt and judge: every typed edge of the evidence sessions scores
21/100, their full text 48/100 (text search 33, the graph agent 12-15). Of 27
span answers, 24 are extracted as entities but only 11 end up on an edge off
the user.

Claude-Session: https://claude.ai/code/session_01BczgXHGBrKsHSQUGB8P8te
…ngested

connect_chunks_to_entities, promote_entity_types_to_labels,
enforce_relation_domain_range and ontology_report ran over the whole
workspace after every ingest, so each session rescanned every entity and
relationship: measured 9 s/session at 8k entities rising to 10 s at 16k on a
4,600-session eval run, heading for ~400k. They now take the ingested chunk
hashes and start from those chunks' entities (MENTIONED_IN), with a
workspace file_path index for the chunk lookup. Unscoped calls keep the old
whole-workspace behaviour for re-projection after an ontology change.

Claude-Session: https://claude.ai/code/session_01BczgXHGBrKsHSQUGB8P8te
…eneric nouns per chunk

Found reading the typed graph built over a 500-session eval batch:
- The model types speaker pronouns Person as well as User, so the mention
  resolver now applies its user rule to them too. Person:'you'/'I'/'assistant'
  had merged, under global identity, into hubs spanning up to 110 unrelated
  sessions and headed the user's own facts.
- An all-lowercase mention of a global type is a generic noun ('home', 'area',
  'city'), so it stays per chunk instead of joining ~50 sessions.

Claude-Session: https://claude.ai/code/session_01BczgXHGBrKsHSQUGB8P8te
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant