{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/semantica-agi/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 }