mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
feat(cookbook): add Datalog-style reasoning end-to-end notebook (#457)
End-to-end example using DatalogReasoner, GraphBuilder, ContextGraph, GraphAnalyzer, ExplanationGenerator, DatalogFact, and DatalogRule. Covers ancestor query, KG dependency analysis, RBAC policy, and org hierarchy. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8d0dce13c5
commit
fdb347fe8a
@@ -0,0 +1,809 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"[](https://colab.research.google.com/github/Hawksight-AI/semantica/blob/main/cookbook/advanced/14_Datalog_Style_Reasoning.ipynb)\n",
|
||||
"\n",
|
||||
"# Datalog-Style Reasoning\n",
|
||||
"\n",
|
||||
"End-to-end guide to Semantica's **`DatalogReasoner`** — a native bottom-up semi-naive fixpoint engine — wired together with `GraphBuilder`, `ContextGraph`, `GraphAnalyzer`, `ExplanationGenerator`, and the supporting data-classes (`DatalogFact`, `DatalogRule`, `InferenceResult`, `Rule`).\n",
|
||||
"\n",
|
||||
"## What you will build\n",
|
||||
"\n",
|
||||
"| Part | Topic | Key classes |\n",
|
||||
"|------|-------|-------------|\n",
|
||||
"| 1 | Core API & EDB/IDB concepts | `DatalogReasoner`, `DatalogFact`, `DatalogRule` |\n",
|
||||
"| 2 | KG → Datalog pipeline | `GraphBuilder`, `GraphAnalyzer`, `DatalogReasoner` |\n",
|
||||
"| 3 | ContextGraph integration | `ContextGraph`, `DatalogReasoner.load_from_graph()` |\n",
|
||||
"| 4 | RBAC access-control policy | `GraphBuilder`, `DatalogReasoner`, `ExplanationGenerator` |\n",
|
||||
"| 5 | Org hierarchy | `ContextGraph`, `DatalogReasoner`, `InferenceResult` |\n",
|
||||
"| 6 | Engine introspection | `DatalogFact`, `DatalogRule` internal state |\n",
|
||||
"\n",
|
||||
"**Related notebooks**\n",
|
||||
"- [08_Reasoning_and_Inference.ipynb](08_Reasoning_and_Inference.ipynb) — high-level `Reasoner` with IF/THEN syntax\n",
|
||||
"- [10_Temporal_Knowledge_Graphs.ipynb](10_Temporal_Knowledge_Graphs.ipynb) — temporal reasoning\n",
|
||||
"\n",
|
||||
"**Documentation**: [Reasoning API](https://semantica.readthedocs.io/reference/reasoning/) | [KG API](https://semantica.readthedocs.io/reference/kg/) | [Context API](https://semantica.readthedocs.io/reference/context/)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install -qU semantica"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Reasoning ──────────────────────────────────────────────────────────────\n",
|
||||
"from semantica.reasoning import (\n",
|
||||
" DatalogReasoner, # native Datalog fixpoint engine\n",
|
||||
" DatalogFact, # frozen dataclass: predicate + args tuple\n",
|
||||
" DatalogRule, # dataclass: head + body (list[BodyAtom])\n",
|
||||
" ExplanationGenerator, # generates NL justifications\n",
|
||||
" InferenceResult, # result dataclass consumed by ExplanationGenerator\n",
|
||||
" Rule, # rule dataclass used by ExplanationGenerator\n",
|
||||
" RuleType, # enum: IMPLICATION | EQUIVALENCE | CONSTRAINT | TRANSFORMATION\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# ── Knowledge Graph ────────────────────────────────────────────────────────\n",
|
||||
"from semantica.kg import (\n",
|
||||
" GraphBuilder, # constructs KG dicts from entity+relationship sources\n",
|
||||
" GraphAnalyzer, # centrality, communities, connectivity, metrics\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# ── Context ────────────────────────────────────────────────────────────────\n",
|
||||
"from semantica.context import ContextGraph # in-memory graph: add_node/add_edge/find_*\n",
|
||||
"\n",
|
||||
"print(\"All Semantica classes imported successfully.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 1 — Core API: EDB Facts, IDB Rules, Fixpoint\n",
|
||||
"\n",
|
||||
"### Datalog in 30 seconds\n",
|
||||
"\n",
|
||||
"| Term | Meaning | Example |\n",
|
||||
"|------|---------|--------|\n",
|
||||
"| EDB (Extensional DB) | Ground facts you assert | `parent(tom, bob)` |\n",
|
||||
"| IDB (Intensional DB) | Facts derived by rules | `ancestor(tom, ann)` |\n",
|
||||
"| Rule (Horn clause) | If body → derive head | `ancestor(X,Y) :- parent(X,Y).` |\n",
|
||||
"| Variable | Uppercase, unified during eval | `X`, `Y`, `Role` |\n",
|
||||
"| Constant | Lowercase, matches literally | `tom`, `admin` |\n",
|
||||
"| Fixpoint | Iterate until no new facts appear | `DatalogReasoner.derive_all()` |\n",
|
||||
"\n",
|
||||
"### The canonical example — transitive ancestry"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Step 1: create engine ──────────────────────────────────────────────────\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"\n",
|
||||
"# ── Step 2: load EDB (ground facts) ───────────────────────────────────────\n",
|
||||
"# Syntax: predicate(constant1, constant2) — constants must be lowercase\n",
|
||||
"edb_facts = [\n",
|
||||
" \"parent(tom, bob)\",\n",
|
||||
" \"parent(bob, ann)\",\n",
|
||||
" \"parent(ann, pat)\",\n",
|
||||
"]\n",
|
||||
"for f in edb_facts:\n",
|
||||
" dr.add_fact(f)\n",
|
||||
"\n",
|
||||
"print(f\"EDB loaded: {len(dr._all_facts)} ground facts\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Step 3: add IDB rules (Horn clauses) ──────────────────────────────────\n",
|
||||
"# Syntax: head(Vars) :- body_atom1(Vars), body_atom2(Vars).\n",
|
||||
"# Variables start with uppercase; trailing '.' is optional\n",
|
||||
"dr.add_rule(\"ancestor(X, Y) :- parent(X, Y).\")\n",
|
||||
"dr.add_rule(\"ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).\") # recursive\n",
|
||||
"\n",
|
||||
"print(f\"Rules loaded: {len(dr._rules)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Step 4: fixpoint evaluation ────────────────────────────────────────────\n",
|
||||
"# derive_all() runs semi-naive bottom-up evaluation until no new facts appear\n",
|
||||
"all_facts: list[str] = dr.derive_all()\n",
|
||||
"\n",
|
||||
"ancestor_strs = sorted(f for f in all_facts if f.startswith(\"ancestor\"))\n",
|
||||
"print(f\"Derived {len(ancestor_strs)} ancestor facts:\")\n",
|
||||
"for f in ancestor_strs:\n",
|
||||
" print(\" \", f)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Step 5: query ──────────────────────────────────────────────────────────\n",
|
||||
"# Use '?varname' placeholders — query() auto-calls derive_all() if needed\n",
|
||||
"# Returns: list[dict] e.g. [{\"Y\": \"bob\"}, {\"Y\": \"ann\"}, {\"Y\": \"pat\"}]\n",
|
||||
"\n",
|
||||
"descendants = dr.query(\"ancestor(tom, ?Y)\")\n",
|
||||
"print(\"All descendants of tom:\", sorted(r[\"Y\"] for r in descendants))\n",
|
||||
"\n",
|
||||
"ancestors_of_pat = dr.query(\"ancestor(?X, pat)\")\n",
|
||||
"print(\"All ancestors of pat: \", sorted(r[\"X\"] for r in ancestors_of_pat))\n",
|
||||
"\n",
|
||||
"all_pairs = dr.query(\"ancestor(?X, ?Y)\")\n",
|
||||
"print(f\"\\nAll ancestor pairs ({len(all_pairs)}):\")\n",
|
||||
"for row in sorted(all_pairs, key=lambda r: (r[\"X\"], r[\"Y\"])):\n",
|
||||
" print(f\" {row['X']:6s} → {row['Y']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 2 — GraphBuilder → DatalogReasoner Pipeline\n",
|
||||
"\n",
|
||||
"`GraphBuilder` constructs a structured `{\"entities\": [...], \"relationships\": [...]}` dict from your data. We then:\n",
|
||||
"\n",
|
||||
"1. Analyse the graph with `GraphAnalyzer` to understand structure.\n",
|
||||
"2. Feed `kg[\"relationships\"]` into `DatalogReasoner` as EDB facts.\n",
|
||||
"3. Apply recursive Datalog rules over the KG."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Build a software-dependency KG ────────────────────────────────────────\n",
|
||||
"entities = [\n",
|
||||
" {\"id\": \"pythonsdk\", \"name\": \"Python SDK\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"restapi\", \"name\": \"REST API\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"authservice\", \"name\": \"Auth Service\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"database\", \"name\": \"Database\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"dashboard\", \"name\": \"Dashboard\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"analytics\", \"name\": \"Analytics\", \"type\": \"Component\"},\n",
|
||||
"]\n",
|
||||
"relationships = [\n",
|
||||
" {\"source\": \"pythonsdk\", \"target\": \"restapi\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"restapi\", \"target\": \"authservice\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"authservice\", \"target\": \"database\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"dashboard\", \"target\": \"restapi\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"dashboard\", \"target\": \"analytics\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"analytics\", \"target\": \"database\", \"type\": \"depends_on\"},\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# GraphBuilder validates, deduplicates, and packages the data\n",
|
||||
"builder = GraphBuilder(merge_entities=True, resolve_conflicts=False)\n",
|
||||
"kg = builder.build([{\"entities\": entities, \"relationships\": relationships}])\n",
|
||||
"\n",
|
||||
"print(f\"KG built — entities: {len(kg['entities'])}, relationships: {len(kg['relationships'])}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Analyse the graph structure before reasoning ───────────────────────────\n",
|
||||
"# GraphAnalyzer provides centrality, communities, connectivity, and metrics\n",
|
||||
"analyzer = GraphAnalyzer()\n",
|
||||
"metrics = analyzer.compute_metrics(graph=kg)\n",
|
||||
"\n",
|
||||
"print(\"Graph structure:\")\n",
|
||||
"print(f\" Nodes : {metrics['num_nodes']}\")\n",
|
||||
"print(f\" Edges : {metrics['num_edges']}\")\n",
|
||||
"if \"density\" in metrics:\n",
|
||||
" print(f\" Density : {metrics['density']:.3f}\")\n",
|
||||
"if \"is_connected\" in metrics:\n",
|
||||
" print(f\" Connected : {metrics['is_connected']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Load KG relationships as EDB facts ────────────────────────────────────\n",
|
||||
"# GraphBuilder output dicts use the same source/target/type shape that\n",
|
||||
"# DatalogReasoner.add_fact() natively understands\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"\n",
|
||||
"for rel in kg[\"relationships\"]:\n",
|
||||
" dr.add_fact(rel) # dict path: {\"source\": ..., \"target\": ..., \"type\": ...}\n",
|
||||
"\n",
|
||||
"print(f\"EDB loaded: {len(dr._all_facts)} dependency facts\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Transitive dependency closure ─────────────────────────────────────────\n",
|
||||
"# 'depends_on' is the predicate name that add_fact inferred from 'type'\n",
|
||||
"dr.add_rule(\"transitive_dep(X, Y) :- depends_on(X, Y).\")\n",
|
||||
"dr.add_rule(\"transitive_dep(X, Y) :- depends_on(X, Z), transitive_dep(Z, Y).\")\n",
|
||||
"\n",
|
||||
"dr.derive_all()\n",
|
||||
"\n",
|
||||
"# Everything that transitively depends on the database\n",
|
||||
"db_deps = sorted(r[\"X\"] for r in dr.query(\"transitive_dep(?X, database)\"))\n",
|
||||
"print(\"Components that transitively depend on Database:\")\n",
|
||||
"for c in db_deps:\n",
|
||||
" print(\" \", c)\n",
|
||||
"\n",
|
||||
"# What does pythonsdk transitively depend on?\n",
|
||||
"sdk_chain = sorted(r[\"Y\"] for r in dr.query(\"transitive_dep(pythonsdk, ?Y)\"))\n",
|
||||
"print(f\"\\nPython SDK full dependency chain: {sdk_chain}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 3 — ContextGraph + `load_from_graph()`\n",
|
||||
"\n",
|
||||
"`DatalogReasoner.load_from_graph(graph)` accepts any `ContextGraph` directly: it calls `graph.find_edges()` and `graph.find_nodes()` and converts each result into EDB facts automatically."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Build an in-memory ContextGraph ───────────────────────────────────────\n",
|
||||
"# ContextGraph.add_node / add_edge are the canonical way to build in-memory KGs\n",
|
||||
"cg = ContextGraph()\n",
|
||||
"\n",
|
||||
"# Nodes\n",
|
||||
"for person in [\"alice\", \"bob\", \"carol\", \"dave\", \"eve\"]:\n",
|
||||
" cg.add_node(person, node_type=\"person\", name=person.capitalize())\n",
|
||||
"\n",
|
||||
"# Directed \"follows\" edges\n",
|
||||
"for src, dst in [(\"alice\", \"bob\"), (\"bob\", \"carol\"), (\"carol\", \"dave\"), (\"alice\", \"eve\"), (\"eve\", \"carol\")]:\n",
|
||||
" cg.add_edge(src, dst, edge_type=\"follows\")\n",
|
||||
"\n",
|
||||
"# Verify the graph built correctly\n",
|
||||
"nodes = cg.find_nodes(node_type=\"person\")\n",
|
||||
"edges = cg.find_edges(edge_type=\"follows\")\n",
|
||||
"print(f\"ContextGraph — nodes: {len(nodes)}, edges: {len(edges)}\")\n",
|
||||
"print(\"Edges:\", [(e.get(\"source\", e.get(\"source_id\")), e.get(\"target\", e.get(\"target_id\"))) for e in edges])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── load_from_graph() ingests the ContextGraph directly ───────────────────\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"n_loaded = dr.load_from_graph(cg) # calls cg.find_edges() + cg.find_nodes() internally\n",
|
||||
"print(f\"Facts loaded from ContextGraph: {n_loaded}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Influence reach via transitive 'follows' ──────────────────────────────\n",
|
||||
"dr.add_rule(\"influence(X, Y) :- follows(X, Y).\")\n",
|
||||
"dr.add_rule(\"influence(X, Y) :- follows(X, Z), influence(Z, Y).\")\n",
|
||||
"\n",
|
||||
"dr.derive_all()\n",
|
||||
"\n",
|
||||
"# Who can alice reach?\n",
|
||||
"alice_reach = sorted(r[\"Y\"] for r in dr.query(\"influence(alice, ?Y)\"))\n",
|
||||
"print(f\"Alice's influence reach : {alice_reach}\")\n",
|
||||
"\n",
|
||||
"# Who can reach dave?\n",
|
||||
"reach_dave = sorted(r[\"X\"] for r in dr.query(\"influence(?X, dave)\"))\n",
|
||||
"print(f\"Who can influence dave : {reach_dave}\")\n",
|
||||
"\n",
|
||||
"# Full influence matrix\n",
|
||||
"all_influence = dr.query(\"influence(?X, ?Y)\")\n",
|
||||
"print(f\"\\nTotal influence pairs: {len(all_influence)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 4 — RBAC Access-Control Policy\n",
|
||||
"\n",
|
||||
"We model a role-based access-control (RBAC) system:\n",
|
||||
"\n",
|
||||
"1. Use `GraphBuilder` to build a structured KG of users, roles, and permissions.\n",
|
||||
"2. Load it into `DatalogReasoner` for policy inference.\n",
|
||||
"3. Use `ExplanationGenerator` to produce audit-ready NL justifications."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Build RBAC graph with GraphBuilder ────────────────────────────────────\n",
|
||||
"rbac_entities = [\n",
|
||||
" # Users\n",
|
||||
" {\"id\": \"alice\", \"type\": \"User\", \"name\": \"Alice\"},\n",
|
||||
" {\"id\": \"bob\", \"type\": \"User\", \"name\": \"Bob\"},\n",
|
||||
" {\"id\": \"carol\", \"type\": \"User\", \"name\": \"Carol\"},\n",
|
||||
" {\"id\": \"dave\", \"type\": \"User\", \"name\": \"Dave\"},\n",
|
||||
" # Roles\n",
|
||||
" {\"id\": \"admin\", \"type\": \"Role\", \"name\": \"Administrator\"},\n",
|
||||
" {\"id\": \"editor\", \"type\": \"Role\", \"name\": \"Editor\"},\n",
|
||||
" {\"id\": \"viewer\", \"type\": \"Role\", \"name\": \"Viewer\"},\n",
|
||||
" # Permissions\n",
|
||||
" {\"id\": \"read\", \"type\": \"Permission\"},\n",
|
||||
" {\"id\": \"write\", \"type\": \"Permission\"},\n",
|
||||
" {\"id\": \"delete\", \"type\": \"Permission\"},\n",
|
||||
" {\"id\": \"manage_users\", \"type\": \"Permission\"},\n",
|
||||
"]\n",
|
||||
"rbac_relationships = [\n",
|
||||
" # User → Role assignments\n",
|
||||
" {\"source\": \"alice\", \"target\": \"admin\", \"type\": \"has_role\"},\n",
|
||||
" {\"source\": \"bob\", \"target\": \"editor\", \"type\": \"has_role\"},\n",
|
||||
" {\"source\": \"carol\", \"target\": \"viewer\", \"type\": \"has_role\"},\n",
|
||||
" {\"source\": \"dave\", \"target\": \"editor\", \"type\": \"has_role\"},\n",
|
||||
" # Role hierarchy (admin inherits from editor, editor from viewer)\n",
|
||||
" {\"source\": \"admin\", \"target\": \"editor\", \"type\": \"role_inherits\"},\n",
|
||||
" {\"source\": \"editor\", \"target\": \"viewer\", \"type\": \"role_inherits\"},\n",
|
||||
" # Role → Permission grants\n",
|
||||
" {\"source\": \"viewer\", \"target\": \"read\", \"type\": \"role_has_perm\"},\n",
|
||||
" {\"source\": \"editor\", \"target\": \"write\", \"type\": \"role_has_perm\"},\n",
|
||||
" {\"source\": \"admin\", \"target\": \"delete\", \"type\": \"role_has_perm\"},\n",
|
||||
" {\"source\": \"admin\", \"target\": \"manage_users\", \"type\": \"role_has_perm\"},\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"builder = GraphBuilder(merge_entities=True, resolve_conflicts=False)\n",
|
||||
"rbac_kg = builder.build([{\"entities\": rbac_entities, \"relationships\": rbac_relationships}])\n",
|
||||
"\n",
|
||||
"print(f\"RBAC KG — entities: {len(rbac_kg['entities'])}, relationships: {len(rbac_kg['relationships'])}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Analyse RBAC graph structure ──────────────────────────────────────────\n",
|
||||
"analyzer = GraphAnalyzer()\n",
|
||||
"metrics = analyzer.compute_metrics(graph=rbac_kg)\n",
|
||||
"centrality = analyzer.calculate_centrality(rbac_kg, centrality_type=\"degree\")\n",
|
||||
"\n",
|
||||
"print(f\"RBAC graph — {metrics['num_nodes']} nodes, {metrics['num_edges']} edges\")\n",
|
||||
"if isinstance(centrality, dict) and \"degree\" in centrality:\n",
|
||||
" top = sorted(centrality[\"degree\"].items(), key=lambda x: x[1], reverse=True)[:3]\n",
|
||||
" print(\"Top-3 nodes by degree centrality:\", top)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Load RBAC KG into DatalogReasoner ────────────────────────────────────\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"\n",
|
||||
"for rel in rbac_kg[\"relationships\"]:\n",
|
||||
" dr.add_fact(rel) # {source, target, type} → predicate(source, target)\n",
|
||||
"\n",
|
||||
"# ── IDB rules: transitive role hierarchy ─────────────────────────────────\n",
|
||||
"dr.add_rule(\"effective_role(R, R2) :- role_inherits(R, R2).\")\n",
|
||||
"dr.add_rule(\"effective_role(R, R2) :- role_inherits(R, Z), effective_role(Z, R2).\")\n",
|
||||
"\n",
|
||||
"# ── IDB rules: inherited permissions ─────────────────────────────────────\n",
|
||||
"dr.add_rule(\"role_can(R, P) :- role_has_perm(R, P).\")\n",
|
||||
"dr.add_rule(\"role_can(R, P) :- effective_role(R, R2), role_has_perm(R2, P).\")\n",
|
||||
"\n",
|
||||
"# ── IDB rules: user effective permissions ────────────────────────────────\n",
|
||||
"dr.add_rule(\"can(U, P) :- has_role(U, R), role_can(R, P).\")\n",
|
||||
"\n",
|
||||
"dr.derive_all()\n",
|
||||
"\n",
|
||||
"print(\"User permissions derived via role-hierarchy inference:\")\n",
|
||||
"for user in [\"alice\", \"bob\", \"carol\", \"dave\"]:\n",
|
||||
" perms = sorted(r[\"P\"] for r in dr.query(f\"can({user}, ?P)\"))\n",
|
||||
" print(f\" {user:6s}: {perms}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── ExplanationGenerator — audit-ready NL justification ──────────────────\n",
|
||||
"# ExplanationGenerator works with InferenceResult objects.\n",
|
||||
"# We construct one manually to represent a derived Datalog conclusion.\n",
|
||||
"\n",
|
||||
"explainer = ExplanationGenerator(detail_level=\"detailed\")\n",
|
||||
"\n",
|
||||
"# Build the Rule object that represents the permission derivation chain\n",
|
||||
"perm_rule = Rule(\n",
|
||||
" rule_id=\"rbac_perm_chain\",\n",
|
||||
" name=\"RBAC permission via role hierarchy\",\n",
|
||||
" conditions=[\"has_role(alice, admin)\", \"effective_role(admin, viewer)\", \"role_has_perm(viewer, read)\"],\n",
|
||||
" conclusion=\"can(alice, read)\",\n",
|
||||
" rule_type=RuleType.IMPLICATION,\n",
|
||||
" confidence=1.0,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Build InferenceResult representing the Datalog conclusion\n",
|
||||
"result = InferenceResult(\n",
|
||||
" conclusion=\"can(alice, read)\",\n",
|
||||
" rule_used=perm_rule,\n",
|
||||
" premises=[\n",
|
||||
" \"has_role(alice, admin)\",\n",
|
||||
" \"role_inherits(admin, editor)\",\n",
|
||||
" \"role_inherits(editor, viewer)\",\n",
|
||||
" \"role_has_perm(viewer, read)\",\n",
|
||||
" ],\n",
|
||||
" confidence=1.0,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Generate NL explanation\n",
|
||||
"explanation = explainer.generate_explanation(result)\n",
|
||||
"print(\"Explanation type :\", explanation.explanation_type)\n",
|
||||
"print(\"Conclusion :\", explanation.conclusion)\n",
|
||||
"print(\"Natural language :\", explanation.natural_language)\n",
|
||||
"print(\"Reasoning steps :\", len(explanation.reasoning_path.steps))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Inverse queries ───────────────────────────────────────────────────────\n",
|
||||
"deleters = sorted(r[\"U\"] for r in dr.query(\"can(?U, delete)\"))\n",
|
||||
"print(\"Who can delete:\", deleters)\n",
|
||||
"\n",
|
||||
"writers = sorted(r[\"U\"] for r in dr.query(\"can(?U, write)\"))\n",
|
||||
"print(\"Who can write: \", writers)\n",
|
||||
"\n",
|
||||
"# All (user, permission) pairs — full policy matrix\n",
|
||||
"all_caps = dr.query(\"can(?U, ?P)\")\n",
|
||||
"print(f\"\\nTotal (user, permission) pairs: {len(all_caps)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 5 — Organisation Hierarchy with ContextGraph\n",
|
||||
"\n",
|
||||
"We model a company org-chart using `ContextGraph` and derive:\n",
|
||||
"- `manages(M, E)` — direct and transitive management\n",
|
||||
"- `skip_level(M, E)` — two hops up the chain\n",
|
||||
"- `same_team(X, Y)` — shared team membership"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── ContextGraph: org chart ────────────────────────────────────────────────\n",
|
||||
"org = ContextGraph()\n",
|
||||
"\n",
|
||||
"# Add employees as nodes with metadata\n",
|
||||
"staff = [\n",
|
||||
" (\"eng1\", \"engineer\", \"backend\"),\n",
|
||||
" (\"eng2\", \"engineer\", \"backend\"),\n",
|
||||
" (\"eng3\", \"engineer\", \"frontend\"),\n",
|
||||
" (\"techlead\", \"lead\", \"engineering\"),\n",
|
||||
" (\"design1\", \"designer\", \"ux\"),\n",
|
||||
" (\"design2\", \"designer\", \"ux\"),\n",
|
||||
" (\"designlead\",\"lead\", \"design\"),\n",
|
||||
" (\"vpeng\", \"vp\", \"engineering\"),\n",
|
||||
" (\"cto\", \"executive\", \"leadership\"),\n",
|
||||
"]\n",
|
||||
"for emp_id, role, team in staff:\n",
|
||||
" org.add_node(emp_id, node_type=\"employee\", role=role, team=team)\n",
|
||||
"\n",
|
||||
"# Reporting lines\n",
|
||||
"reports_to = [\n",
|
||||
" (\"eng1\", \"techlead\"), (\"eng2\", \"techlead\"), (\"eng3\", \"techlead\"),\n",
|
||||
" (\"techlead\", \"vpeng\"),\n",
|
||||
" (\"design1\", \"designlead\"), (\"design2\", \"designlead\"),\n",
|
||||
" (\"designlead\", \"vpeng\"),\n",
|
||||
" (\"vpeng\", \"cto\"),\n",
|
||||
"]\n",
|
||||
"for employee, manager in reports_to:\n",
|
||||
" org.add_edge(employee, manager, edge_type=\"reports_to\")\n",
|
||||
"\n",
|
||||
"# Team membership edges\n",
|
||||
"teams = [\n",
|
||||
" (\"eng1\", \"backend\"), (\"eng2\", \"backend\"), (\"eng3\", \"frontend\"),\n",
|
||||
" (\"design1\", \"ux\"), (\"design2\", \"ux\"),\n",
|
||||
"]\n",
|
||||
"for emp, team in teams:\n",
|
||||
" org.add_edge(emp, team, edge_type=\"in_team\")\n",
|
||||
" if not org.find_nodes(node_type=\"team\"):\n",
|
||||
" org.add_node(team, node_type=\"team\")\n",
|
||||
"\n",
|
||||
"print(f\"ContextGraph — nodes: {len(org.find_nodes())}, edges: {len(org.find_edges())}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Load org chart into DatalogReasoner ───────────────────────────────────\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"n = dr.load_from_graph(org) # uses org.find_edges() + org.find_nodes()\n",
|
||||
"print(f\"Facts loaded via load_from_graph(): {n}\")\n",
|
||||
"\n",
|
||||
"# ── IDB rules ─────────────────────────────────────────────────────────────\n",
|
||||
"# Transitive management chain\n",
|
||||
"dr.add_rule(\"manages(M, E) :- reports_to(E, M).\")\n",
|
||||
"dr.add_rule(\"manages(M, E) :- reports_to(E, Z), manages(M, Z).\")\n",
|
||||
"\n",
|
||||
"# Skip-level: exactly two reporting hops\n",
|
||||
"dr.add_rule(\"skip_level(M, E) :- reports_to(E, Z), reports_to(Z, M).\")\n",
|
||||
"\n",
|
||||
"# Same team\n",
|
||||
"dr.add_rule(\"same_team(X, Y) :- in_team(X, T), in_team(Y, T).\")\n",
|
||||
"\n",
|
||||
"dr.derive_all()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Query org hierarchy ────────────────────────────────────────────────────\n",
|
||||
"# Everyone under CTO\n",
|
||||
"under_cto = sorted(r[\"E\"] for r in dr.query(\"manages(cto, ?E)\"))\n",
|
||||
"print(f\"CTO manages ({len(under_cto)} people): {under_cto}\")\n",
|
||||
"\n",
|
||||
"# VP Eng's direct + indirect reports\n",
|
||||
"under_vp = sorted(r[\"E\"] for r in dr.query(\"manages(vpeng, ?E)\"))\n",
|
||||
"print(f\"VP Eng manages : {under_vp}\")\n",
|
||||
"\n",
|
||||
"# Skip-level reports to CTO (people two hops below CTO)\n",
|
||||
"skip = sorted(r[\"E\"] for r in dr.query(\"skip_level(cto, ?E)\"))\n",
|
||||
"print(f\"CTO skip-level reports : {skip}\")\n",
|
||||
"\n",
|
||||
"# eng1's teammates\n",
|
||||
"mates = [r[\"Y\"] for r in dr.query(\"same_team(eng1, ?Y)\") if r[\"Y\"] != \"eng1\"]\n",
|
||||
"print(f\"eng1's teammates : {sorted(mates)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Build an InferenceResult and explain an org query ────────────────────\n",
|
||||
"explainer = ExplanationGenerator(detail_level=\"verbose\")\n",
|
||||
"\n",
|
||||
"mgmt_rule = Rule(\n",
|
||||
" rule_id=\"transitive_manages\",\n",
|
||||
" name=\"Transitive management chain\",\n",
|
||||
" conditions=[\"reports_to(eng1, techlead)\", \"manages(vpeng, techlead)\"],\n",
|
||||
" conclusion=\"manages(vpeng, eng1)\",\n",
|
||||
" rule_type=RuleType.IMPLICATION,\n",
|
||||
" confidence=1.0,\n",
|
||||
")\n",
|
||||
"result = InferenceResult(\n",
|
||||
" conclusion=\"manages(vpeng, eng1)\",\n",
|
||||
" rule_used=mgmt_rule,\n",
|
||||
" premises=[\"reports_to(eng1, techlead)\", \"reports_to(techlead, vpeng)\"],\n",
|
||||
" confidence=1.0,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"exp = explainer.generate_explanation(result)\n",
|
||||
"print(exp.natural_language)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 6 — Engine Introspection: DatalogFact & DatalogRule\n",
|
||||
"\n",
|
||||
"After reasoning, the engine's internal state is fully accessible via `DatalogFact` and `DatalogRule` data-classes. Use this for auditing, debugging, or downstream export."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Inspect DatalogRule objects ────────────────────────────────────────────\n",
|
||||
"# dr._rules → List[DatalogRule]\n",
|
||||
"# DatalogRule.head_predicate, .head_args, .body (body = List[BodyAtom])\n",
|
||||
"print(\"Rules in engine:\")\n",
|
||||
"for rule in dr._rules:\n",
|
||||
" body_str = \", \".join(\n",
|
||||
" f\"{atom.predicate}({', '.join(atom.args)})\"\n",
|
||||
" for atom in rule.body\n",
|
||||
" )\n",
|
||||
" head_str = f\"{rule.head_predicate}({', '.join(rule.head_args)})\"\n",
|
||||
" print(f\" {head_str} :- {body_str}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Inspect DatalogFact objects ────────────────────────────────────────────\n",
|
||||
"# dr._all_facts → Set[DatalogFact] (EDB + IDB combined after derive_all)\n",
|
||||
"# dr._fact_index → Dict[predicate, Set[DatalogFact]]\n",
|
||||
"\n",
|
||||
"from collections import Counter\n",
|
||||
"\n",
|
||||
"# Count facts per predicate\n",
|
||||
"predicate_counts = Counter(f.predicate for f in dr._all_facts)\n",
|
||||
"print(\"Facts per predicate (EDB + derived IDB):\")\n",
|
||||
"for pred, count in sorted(predicate_counts.items()):\n",
|
||||
" print(f\" {pred:20s}: {count}\")\n",
|
||||
"print(f\"\\n TOTAL: {len(dr._all_facts)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Separate EDB from IDB ─────────────────────────────────────────────────\n",
|
||||
"# EDB predicates are the ones we added via add_fact (not derived by rules)\n",
|
||||
"idb_predicates = {rule.head_predicate for rule in dr._rules}\n",
|
||||
"edb_predicates = {f.predicate for f in dr._all_facts} - idb_predicates\n",
|
||||
"\n",
|
||||
"print(f\"EDB predicates (base facts) : {sorted(edb_predicates)}\")\n",
|
||||
"print(f\"IDB predicates (derived) : {sorted(idb_predicates)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Sample DatalogFact structure ──────────────────────────────────────────\n",
|
||||
"# DatalogFact is a frozen dataclass: predicate: str, args: Tuple[str, ...]\n",
|
||||
"manages_facts = sorted(dr._fact_index.get(\"manages\", []), key=lambda f: f.args)\n",
|
||||
"print(f\"First 5 'manages' DatalogFact objects ({len(manages_facts)} total):\")\n",
|
||||
"for fact in manages_facts[:5]:\n",
|
||||
" # Access predicate and args directly from the dataclass\n",
|
||||
" print(f\" DatalogFact(predicate={fact.predicate!r}, args={fact.args})\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── clear() resets the engine completely ─────────────────────────────────\n",
|
||||
"print(f\"Facts before clear(): {len(dr._all_facts)}\")\n",
|
||||
"dr.clear()\n",
|
||||
"print(f\"Facts after clear(): {len(dr._all_facts)}\")\n",
|
||||
"print(f\"Rules after clear(): {len(dr._rules)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## API Summary\n",
|
||||
"\n",
|
||||
"### DatalogReasoner\n",
|
||||
"\n",
|
||||
"| Method | Input | Output | Notes |\n",
|
||||
"|--------|-------|--------|-------|\n",
|
||||
"| `add_fact(f)` | `str` or `dict` | `None` | string: `\"pred(a, b)\"` · dict: `{source, target, type}` |\n",
|
||||
"| `add_rule(s)` | `str` | `None` | Horn clause: `\"head(X) :- body(X, Y).\"` |\n",
|
||||
"| `derive_all()` | — | `list[str]` | semi-naive fixpoint; idempotent |\n",
|
||||
"| `query(pat)` | `str` | `list[dict]` | `\"pred(a, ?Y)\"` → `[{\"Y\": ...}]` |\n",
|
||||
"| `load_from_graph(g)` | `ContextGraph` | `int` | facts loaded count |\n",
|
||||
"| `clear()` | — | `None` | resets engine |\n",
|
||||
"\n",
|
||||
"### Syntax rules\n",
|
||||
"\n",
|
||||
"| Item | Rule | Example |\n",
|
||||
"|------|------|---------|\n",
|
||||
"| Variable | Starts **uppercase** | `X`, `Role`, `Parent` |\n",
|
||||
"| Constant | All **lowercase** | `tom`, `admin`, `database` |\n",
|
||||
"| Query var | Prefix `?` | `?X`, `?Y`, `?Role` |\n",
|
||||
"| Rule body | `:-` separator, comma between atoms | `head(X) :- a(X, Z), b(Z, Y).` |\n",
|
||||
"\n",
|
||||
"### Class map\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"GraphBuilder.build() → kg dict {entities, relationships}\n",
|
||||
" ↓ kg[\"relationships\"] → dr.add_fact(rel)\n",
|
||||
" \n",
|
||||
"ContextGraph.add_node/add_edge → in-memory graph\n",
|
||||
" ↓ dr.load_from_graph(cg)\n",
|
||||
" \n",
|
||||
"DatalogReasoner.add_rule() → Horn clause rules\n",
|
||||
"DatalogReasoner.derive_all() → semi-naive fixpoint\n",
|
||||
"DatalogReasoner.query() → result rows\n",
|
||||
" ↓ build InferenceResult\n",
|
||||
" \n",
|
||||
"ExplanationGenerator → natural language justification\n",
|
||||
"GraphAnalyzer → graph structure metrics pre/post reasoning\n",
|
||||
"DatalogFact / DatalogRule → introspect engine state\n",
|
||||
"```"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Reference in New Issue
Block a user