From fdb347fe8ae48cdc672e15dec4f3e99dcc532291 Mon Sep 17 00:00:00 2001 From: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:43:04 +0530 Subject: [PATCH] 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 --- .../advanced/14_Datalog_Style_Reasoning.ipynb | 809 ++++++++++++++++++ 1 file changed, 809 insertions(+) create mode 100644 cookbook/advanced/14_Datalog_Style_Reasoning.ipynb diff --git a/cookbook/advanced/14_Datalog_Style_Reasoning.ipynb b/cookbook/advanced/14_Datalog_Style_Reasoning.ipynb new file mode 100644 index 00000000..5382c03e --- /dev/null +++ b/cookbook/advanced/14_Datalog_Style_Reasoning.ipynb @@ -0,0 +1,809 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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 +}