Blog / How-to guides

Facts that follow: symmetric, inverse and transitive relationships, each answer with its proof

21 October 2026 · Semurg · 4 min read · Markdown

knowledge-graphreasoningneurosymbolicontologypythonhow-to

Some facts follow from others by what a relationship means. If Ada is married to Charles, Charles is married to Ada. If Byron is the parent of Ada, Ada is the child of Byron. If the Graph Team is part of Research and Research is part of Acme, the Graph Team is part of Acme. You should not have to store each of those twice, and when the memory answers with one, it should show its work.

Three rules. Symmetric: a married_to b gives b married_to a. Inverse: a parent_of b gives b child_of a. Transitive: a part_of b and b part_of c give a part_of c. Each derived answer names its rule and the stored facts it rests on.Stored factsSymmetric: married_toInverse: parent_of andchild_ofTransitive: part_ofA derived answer: itsrule and its proofThree rules. Symmetric: a married_to b gives b married_to a. Inverse: a parent_of b gives b child_of a. Transitive: a part_of b and b part_of c give a part_of c. Each derived answer names its rule and the stored facts it rests on.Stored factsSymmetric: married_toInverse: parent_of andchild_ofTransitive: part_ofA derived answer: itsrule and its proof

Store four facts, read the ones that follow

The program below ran as written against a fresh node of release r11.121.20261021.3619d35, with semurg 0.1.1 from PyPI. Save it as reasoning.py (with a node and SEMURG_API_TOKEN set, as in the PyPI quickstart):

python
import os
from semurg import Client

with Client("http://127.0.0.1:4000", token=os.environ["SEMURG_API_TOKEN"]) as mem:
    mem.remember("org", facts={
        "entities": [{"name": "Ada", "type": "person"}, {"name": "Charles", "type": "person"},
                     {"name": "Byron", "type": "person"}, {"name": "Graph Team", "type": "team"},
                     {"name": "Research", "type": "department"}, {"name": "Acme", "type": "company"}],
        "relations": [{"source": "Ada", "target": "Charles", "type": "married_to"},
                      {"source": "Byron", "target": "Ada", "type": "parent_of"},
                      {"source": "Graph Team", "target": "Research", "type": "part_of"},
                      {"source": "Research", "target": "Acme", "type": "part_of"}],
    })

    for who in ["Charles", "Ada", "Graph Team"]:
        for item in mem.recall("org", subject=who)["items"]:
            if item["kind"] == "fact" and item.get("derived"):
                proof = " + ".join(f'{d["subject"]} {d["relation"]} {d["object"]}' for d in item["derivation"])
                print(f'{who} {item["relation"]} {item["text"]}  ({item["rule"]}: {proof})')

It prints the facts that were never stored, each with the rule that produced it and the stored facts it rests on:

text
Charles married_to Ada  (symmetric: Ada married_to Charles)
Ada child_of Byron  (inverse: Byron parent_of Ada)
Graph Team part_of Acme  (transitive: Graph Team part_of Research + Research part_of Acme)

How the answers are marked

A derived fact comes back in the same list as stored ones, so an agent reads one answer, but it is never passed off as stored. It has derived: true, its rule (symmetric, inverse or transitive), and a derivation: the stored facts it follows from, each one recallable on its own. Its citation says how it was found (found_by.reasoning), where a stored fact cites the source it came from. That makes every derived answer checkable: follow the derivation back to the stored facts and their sources.

Which relationships carry a meaning

The rules come with the relationship's name. This is the whole list in the current release:

RuleRelationshipsWhat follows
symmetricmarried_to, spouse_of, sibling_of, partner_of, neighbor_of (or neighbour_of), adjacent_to, borders, colleague_of, twin_ofb relates to a when a relates to b
inverseparent_of and child_of, owns and owned_by, employs and employed_by, manages and managed_by, acquired and acquired_by, founded and founded_by, has_part (or contains) and part_of, has_subsidiary and subsidiary_of, teaches and taught_by, supervises and supervised_by, leads and led_by, authored and authored_by, created and created_byeach side of the pair from the other
transitivepart_of, located_in, contained_in, subsidiary_of, ancestor_of, descendant_of, subclass_of, withina chain of up to 6 links collapses to one answer

Names are matched in any letter case and with spaces or hyphens in place of underscores: MARRIED_TO, Part of and part-of all match. So in the example above, recalling Research also answers that Research has part the Graph Team, by the inverse of part_of. The list is fixed: the rules do not come from the ontology, and you cannot add your own today. A relationship outside it is stored and returned exactly as sent, with nothing derived from it. The recall tool's description over MCP names the most common of these rules, so an agent learns them from the tool itself.

Why it matters

An agent asked "who is Charles married to?" gets an answer even though only Ada's side was stored, and the answer carries its proof. That is the difference between a memory that matches strings and one that knows what its relationships mean, and it is why the types and relation names your agents choose matter. The classes and relation types a scope has used are listed by memory_status; see the automated ontology.

Earlier in this series: Graph RAG that says when it does not know and memory with a timeline. Next: scopes, separate memories shared on purpose.

Related