mempalace.kg_predicate_norm
Source: mempalace/kg_predicate_norm.py
Predicate normalization for the AGE knowledge graph (issue #50).
The LLM triple extractor emits ~1000+ distinct relation_type strings. Three classes of contamination bloat the predicate vocabulary far past the underlying semantic relation count:
- Code tokens treated as predicates (
appendchild,createelement,executemany,setattribute,getelementbyid) — JS/Python API method names the extractor pulled out of source-code drawers. These should be dropped. - Near-synonyms not collapsed (
is/is_a/is_an_instance_of;was_a/is_a_kind_of) — canonicalized to one relation type. - Grammatical fragments with negation/punctuation glued in (
don't_adapt,aren't_merged,'doesn't_appear') — apostrophes and quotes are stripped and the negation polarity is denormalized into anot_<base>form rather than left as an arbitrary contraction.
This is a pure module — no DB, no AGE imports, no network. The single public entry point is :func:normalize_predicate, which returns the canonical predicate string or None to signal "drop this triple". That makes it trivially unit-testable and safe to run as a read-only dry-run pass over the live vocabulary without touching the graph.
Wiring this into the write path is the daemon's choice and must be opt-in; this module never mutates anything on its own.
Functions
normalize_predicate
def normalize_predicate(raw: str) -> Optional[str]Normalize a raw extractor predicate to its canonical form.
Returns the canonical predicate string, or None to signal that the triple should be dropped (code token, empty, or punctuation-only input).
Pipeline:
- fold — lowercase, snake_case, strip quotes/apostrophes (class 3 prep)
- drop if empty or a known/heuristic code or shell token (class 1)
- drop if a content-free function word / modal (class 1c, issue #45)
- strip negation prefix, remember polarity (class 3)
- canonicalize the base via the synonym map (class 2)
- re-apply
not_prefix if it was negated
The negation prefix is applied after canonicalization so that doesn't_appear and does_not_appear both land on not_appear, and a negated synonym (is not a part of → base a_part_of → part_of) collapses to not_part_of.
Note the base is whatever remains after the negation prefix is peeled: isn't_a strips isnt_ leaving base a (not in SYNONYM_MAP), so it yields not_a, not not_is_a.
