Back to use cases
advanced2-3 days

3D Knowledge Graph of EU Regulation

Extract entities and relationships from EU legal documents and visualize them as an interactive 3D knowledge graph.

GraphRAGKnowledge Graph3D Visualization

What you'll build

EU regulation forms a dense web of cross-references: directives cite regulations, CJEU rulings interpret treaty articles, and national courts apply preliminary rulings. Visualizing this structure as a knowledge graph reveals relationships that are invisible in linear document lists. This project uses the Moonlit API to collect EU legal documents, extract entities (courts, articles, directives, case numbers) and relationships (cites, interprets, amends, repeals), and build a graph database. The graph is then rendered as an interactive 3D visualization using Three.js where nodes are documents and edges are legal relationships. The result is a tool that lets researchers explore the regulatory landscape spatially -- seeing at a glance which directives are most cited, which CJEU rulings are central to a legal domain, and how different pieces of legislation interconnect.

Architecture


  ┌─────────────┐    ┌────────────────┐    ┌───────────────┐
  │ Moonlit API │──▶│ Entity         │──▶│ Graph DB      │
  │ search +    │    │ Extraction     │    │ (Neo4j)       │
  │ retrieve    │    │ (LLM-powered)  │    │               │
  └─────────────┘    └────────────────┘    └───────┬───────┘
                                                  │
                                                  ▼
                                          ┌───────────────┐
                                          │ 3D Force      │
                                          │ Graph (Three) │
                                          │   o---o       │
                                          │  / \ |        │
                                          │ o   o-o       │
                                          └───────────────┘

Prerequisites

  • A Moonlit API key
  • Python 3.10+ with requests, anthropic, neo4j-driver
  • Neo4j database (local Docker or Aura cloud)
  • Node.js project with three.js and 3d-force-graph
  • An Anthropic API key for entity extraction

Step-by-step

1

Collect documents from a regulatory domain

Use the Moonlit search API to collect all EU documents in a specific regulatory area. Paginate through results to build a comprehensive corpus.

import requests

MOONLIT_KEY = "your-api-key"
BASE_URL = "https://api.moonlit.ai/v1.1"

def collect_corpus(query: str, max_docs: int = 200) -> list[dict]:
    """Collect documents by paginating through search results."""
    docs = []
    page = 1
    while len(docs) < max_docs:
        response = requests.post(
            f"{BASE_URL}/search/hybrid_search",
            headers={
                "Ocp-Apim-Subscription-Key": MOONLIT_KEY,
                "Content-Type": "application/json",
            },
            json={
                "query": query,
                "jurisdictions": ["European Union"],
                "documentTypes": ["case_law", "legislation"],
                "num_results": 50,
                "page": page,
            },
        )
        data = response.json()
        results = data.get("result", {}).get("results", [])
        if not results:
            break
        docs.extend(results)
        page += 1

    # Retrieve full text for each document
    full_docs = []
    for doc in docs[:max_docs]:
        detail = requests.get(
            f"{BASE_URL}/document/retrieve_document",
            headers={"Ocp-Apim-Subscription-Key": MOONLIT_KEY},
            params={"DocumentIdentifier": doc["identifier"]},
        )
        if detail.ok:
            full_docs.append(detail.json())
    return full_docs

corpus = collect_corpus("Digital Services Act platform liability", max_docs=100)
print(f"Collected {len(corpus)} documents")
2

Extract entities and relationships with an LLM

Use Claude to extract structured entities (directives, regulations, articles, courts, case numbers) and relationships (cites, interprets, amends) from each document.

import anthropic
import json

client = anthropic.Anthropic()

EXTRACTION_PROMPT = """Extract legal entities and relationships from this document.
Return JSON with this structure:
{
  "entities": [
    {"id": "...", "type": "directive|regulation|case|article|court", "label": "..."}
  ],
  "relationships": [
    {"source": "...", "target": "...", "type": "cites|interprets|amends|repeals|applies"}
  ]
}

Document:
"""

def extract_graph(doc: dict) -> dict:
    text = doc.get("text", doc.get("summary", ""))[:4000]
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,
        messages=[{
            "role": "user",
            "content": f"{EXTRACTION_PROMPT}\nID: {doc['identifier']}\nTitle: {doc['title']}\n\n{text}",
        }],
    )
    try:
        return json.loads(response.content[0].text)
    except json.JSONDecodeError:
        return {"entities": [], "relationships": []}
3

Load the graph into Neo4j

Insert extracted entities as nodes and relationships as edges into a Neo4j graph database. Merge on entity ID to deduplicate.

from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))

def load_graph(extracted: dict):
    with driver.session() as session:
        for entity in extracted["entities"]:
            session.run(
                "MERGE (n:LegalEntity {id: $id}) "
                "SET n.type = $type, n.label = $label",
                id=entity["id"], type=entity["type"], label=entity["label"],
            )
        for rel in extracted["relationships"]:
            session.run(
                "MATCH (a:LegalEntity {id: $src}), (b:LegalEntity {id: $tgt}) "
                "MERGE (a)-[r:LEGAL_REL {type: $type}]->(b)",
                src=rel["source"], tgt=rel["target"], type=rel["type"],
            )

for doc in corpus:
    graph_data = extract_graph(doc)
    load_graph(graph_data)
    print(f"Processed: {doc['identifier']}")
4

Visualize in 3D with Three.js

Export the graph from Neo4j and render it using the 3d-force-graph library. Color-code nodes by entity type and scale them by citation count.

import ForceGraph3D from "3d-force-graph";

const graphData = await fetch("/api/graph").then((r) => r.json());

const Graph = ForceGraph3D()(document.getElementById("graph-container"))
  .graphData(graphData)
  .nodeLabel("label")
  .nodeColor((node) => {
    const colors = {
      directive: "#4F46E5",
      regulation: "#059669",
      case: "#DC2626",
      article: "#D97706",
      court: "#7C3AED",
    };
    return colors[node.type] || "#6B7280";
  })
  .nodeVal((node) => Math.max(node.citations || 1, 1))
  .linkLabel("type")
  .linkColor("#CBD5E1")
  .linkWidth(1)
  .onNodeClick((node) => {
    window.open(node.url, "_blank");
  });

What's next