GraphSight
A causal event graph for exploring why complex world events happen: directed cause-and-effect chains, contested evidence, and multiple analytical lenses.
What I built
GraphSight models history as a graph. Events are nodes (geopolitical shifts, economic decisions, technology milestones), and the edges between them are directed, typed causal links: DirectlyCaused, EnabledConditionsFor, ContributedTo, Contested, Correlated. Each edge carries its strength, an explanation, a perspective, and whether the causal claim itself is contested.
The core unit of exploration is the chain: a named investigation anchored at a root question like “why did the 2008 financial crisis happen?”. You get an initial 3–5 node graph, expand it to full depth, or AI-expand any single node: the model proposes the next 5–7 events with relationship types and directions. The same subgraph can be re-read through different perspectives (Mainstream, Geopolitical, Structural, Economic, Revisionist), and contested edges surface the opposing view right next to the claim.
Why I built it
News is consumed as isolated fragments: a headline today, a follow-up next week, a background piece somewhere else. The connections (what actually led to what) live in the reader’s head, and most tools don’t even try to represent them.
Causes are inherently a graph, not a feed. Once events are nodes and causes are typed, directional edges, you can ask things a timeline can’t answer: which upstream factors converge on this event, how confident each link is, and how the same chain reads through a different analytical lens. Making the AI assign a perspective to every edge, and flag contested ones with an opposing view, was the difference between a graph that pretends history is settled and one that shows the argument.
How events get into the graph
- Search runs in three phases: keyword search in Neo4j, then semantic search in Qdrant, and (only if nothing relevant is found) AI generation kicks in.
- The AI sidecar runs one structured mega-prompt: it returns events with dates, domains, confidence scores and real source URLs, plus edges with relationship types, strengths, perspectives and opposing views; then JSON salvaging and field coercion normalize whatever the model produces.
- Generated nodes are persisted to Neo4j and indexed into Qdrant, so the next search hits the database instead of calling a model. Bulk ingestion is idempotent (UNWIND + MERGE), and a cycle check over the causal paths blocks contradictory edges before they land.
- Confidence is a first-class value: every event carries a score mapped to human-readable levels, and every causal claim says how contested it is.
How you explore it
The frontend is a Next.js app with a hand-rolled SVG graph canvas (no graph library, just a custom force layout with manual pan and zoom), backed by a timeline bar that filters visible nodes and edges by date, a strength legend, and a what-if simulation that removes a node so you can see which downstream effects fall apart.
Authentication is JWT with Redis-backed refresh tokens and Cloudflare Turnstile on the forms. Users can save investigations to their history, and rate limiting keeps AI expansion fair at ten requests per minute per user.
How it’s bundled and hosted
The stack is Docker Compose: the .NET API, a FastAPI AI sidecar, PostgreSQL, Neo4j 5.18 with APOC, Redis, Qdrant, and Ollama with a one-shot container that pulls the models on first run. A setup script takes a fresh clone to a running system: env, images, Neo4j constraints and seed data, EF Core migrations, model pulls.
It runs on my home server behind a Cloudflare Tunnel: Cloudflare fronts the domain, cloudflared hands off to Caddy, and Caddy routes to the API and frontend over an internal Docker network. Only Caddy and SSH are exposed on the host (every database port is loopback-only), with uptime monitoring and nightly restic backups to Backblaze B2.
One design decision I keep coming back to: bring-your-own-key. Every user can plug in their own provider key (Grok, OpenAI, Claude, Gemini), stored encrypted per user. No key? The request transparently falls back to the local Ollama model. The server never holds cloud API keys for generation: everyone gets either their own model or mine, local and free.
The stack: who does what
- .NET 8
- The backend: Clean Architecture with CQRS over MediatR, separate command and query pipelines, domain events, EF Core and FluentValidation.
- Neo4j
- The graph store: EventNode and chain nodes, typed CAUSED edges with perspective and contested flags, APOC, and cycle detection over causal paths.
- FastAPI
- The AI sidecar: event extraction, causal-relationship analysis, chain expansion and async knowledge-graph generation jobs.
- Qdrant
- Semantic search: nomic-embed-text embeddings over indexed events, the second phase of the search pipeline.
- Ollama
- The local model: the default brain for generation and the BYOK fallback, so the app works for users with no API key.
- Next.js 16
- The frontend: hand-rolled SVG graph canvas with force layout, timeline filtering, what-if simulation and BYOK key management.
- PostgreSQL + Redis
- Identity, saved chains, encrypted per-user API keys, prompt audit logs, refresh tokens and rate limiting.
- Docker
- Packaging: the full multi-service stack, plus a setup script that goes from clone to running system in one command.