A narrative intelligence platform for building, understanding, and reasoning about fictional worlds.
The state of a fiction — who knows whom, who belongs to which faction, where
something happened and who was there — usually lives in a writer's head or
scattered across flat documents, where nothing can be asked of it. Narrative
Mind stores that state as an actual graph and puts a workspace over it: four
entity types (Character, Location, Faction, Event), typed relationships
between them, traversal queries across the result, LLM endpoints that write
prose from an entity or extract entities and relationships out of a passage,
and Graph RAG — every entity carries a vector embedding of its own text, and
POST /ai/ask answers a natural-language question about the world with a
grounded, cited answer: vector search seeds the question, a graph traversal
expands the seeds into their surrounding relationships, and the model is
constrained to that context and validated against it.
The shipped capability is the Narrative Graph (V1), implemented end-to-end across both halves of this repository.
| Path | What it is |
|---|---|
backend/ |
Async FastAPI service over Neo4j — entities, relationships, graph traversal, AI endpoints, JWT auth. Setup and full API reference: backend/README.md |
frontend/ |
React + TypeScript workspace — a desktop-class, dark-only UI over the API. Setup and feature list: frontend/README.md |
docs/frontend/ |
The frontend's design documents and its per-milestone build log (see Documents) |
docker-compose.yml |
Neo4j 5.26 for local development |
The two halves deploy independently as separate Vercel projects; neither imports from the other, and the API contract is the only coupling.
Prerequisites: Python 3.12+, uv, Node.js
20.19+, Docker (or any reachable Neo4j 5.18+ instance — needed for Graph
RAG's vector.similarity.cosine()), and Ollama if you
want the /ai endpoints (chat and embeddings both run through it locally by
default).
1. Neo4j — from the repository root:
docker compose up -d neo4jBolt on localhost:7687, browser UI on localhost:7474, credentials
neo4j / password123 (matching backend/.env.example). Constraints and
indexes are applied automatically on API startup.
2. Backend — in backend/:
uv sync
cp .env.example .env
uv run uvicorn narrative_mind.main:app --reload # http://localhost:8000Interactive API docs at http://localhost:8000/docs. For the /ai endpoints,
also ollama pull llama3.2:3b (chat) and ollama pull nomic-embed-text-v2-moe
(embeddings, powers Graph RAG). See
backend/README.md
for the Ollama-vs-Groq/Google provider choices — local development needs only
Ollama, no Groq or Google account.
3. Frontend — in frontend/:
npm install
cp .env.example .env # VITE_API_BASE_URL, defaults to http://localhost:8000
npm run dev # http://localhost:5173Every route except /health and /auth/* requires a token, so register an
account from the UI before anything else will load. Registering also gives the
new account its own copy of the Verge starter world — 10 characters, 6
locations, 5 factions, 6 events and the relationships between them — so the first
screen after signing in is a populated graph rather than an empty one.
Resetting a world. Editing is destructive by design; to put an account's
world back to the starter state, from backend/:
uv run python scripts/seed_world.py you@example.comThat replaces only that account's world. Other accounts and every login, including the target's own, are untouched.
Set SEED_NEW_USER_WORLD=false to have accounts start empty instead. The test
suite does exactly that — a world arriving unasked is indistinguishable from data
a test created.
- Auth — register, log in, HS256 bearer tokens; every route but
/healthand/auth/*is protected. No refresh endpoint in V1. - Per-account worlds — every entity carries an
owner_idand every query filters on it, so accounts cannot read, edit, delete or traverse into each other's worlds; another account's entity returns 404, not 403. The owner is a constructor argument on the repositories, injected from the token inapi/deps.py, so no service or route passes it and none can forget to. - Entity management — full CRUD for characters, locations, factions, and events, with paginated lists (search, sort, and a categorical filter for the types that have one), rendered from a single entity engine on the frontend.
- Relationships —
KNOWS,MEMBER_OF,LOCATED_IN,PARTICIPATED_IN, written from a character;KNOWSedges may carry a sentiment. - Graph — ego-network traversal (depth 1–3) and shortest path between two characters, rendered as an interactive Cytoscape canvas with connect/edit gestures.
- AI —
POST /ai/describe(prose from an entity) andPOST /ai/extract(schema-constrained entity/relationship extraction from a passage), behind a swappable chat provider: Ollama locally, Groq in deployment. - Graph RAG — every entity embeds its own text on create/update (a second,
independent swappable provider: Ollama locally, Google in deployment, since
Groq has no embeddings endpoint).
POST /ai/retrieveruns owner-scoped vector search plus graph expansion into a bounded, citable context block;POST /ai/askgenerates an answer from it and strips any citation the model invents. Isolation is structural, not just filtered: one account's search can never surface another's entity, even a byte-identical one. - AI in the workspace — all four
/ai/*endpoints are reachable from the UI as one surface: an Ask page, plus a ⌘I dock that opens beside whatever screen you are on, rendering the grounded answer with its citations as links into the entities they name and the retrieval trace behind it; an Extract page that reads a passage and resolves what it finds against the world you already have (read-only — nothing is written); and a Suggest button in every entity form's description field.
Roadmap capabilities named in the design documents but absent from the code: Timeline, Rich Text Editing, World Encyclopedia, Consistency Checking, and Knowledge Search.
Also outstanding:
- World overview and global search (frontend M7) — the workspace's landing
page is a navigational welcome screen, not a world summary, and the ⌘K
palette searches commands and destinations but not entities.
frontend/src/features/world/is still a reserved, empty slice. - Frontend polish and accessibility pass (M8) — much of it landed
opportunistically alongside later milestones (keyboard map,
Kbdhints, reduced-motion handling, token-driven states), but the milestone has never been run as its own audit. - No component tests on the frontend. The suite is pure TypeScript and stops at the renderer boundary; rendering, forms, and interaction are verified by hand.
- One world per account, and no switcher — an account cannot keep two separate worlds or share one with somebody else.
- Extraction does not write back.
/ai/extractreturns names, not ids, and persists nothing, so the Extract page proposes and matches but cannot create.
| Backend | Python 3.12, FastAPI (async), Neo4j 5.26 via Bolt (vector similarity search), Pydantic v2, PyJWT, pwdlib/argon2, Ollama/Groq (chat), Ollama/Google Gemini (embeddings), uv, ruff, pytest |
| Frontend | React 19, TypeScript, Vite, Tailwind v4 (CSS-first), shadcn/ui + Radix, TanStack Query & Table, Zustand, React Hook Form + Zod, Cytoscape, cmdk, Sonner, Vitest + MSW |
| Deployed on | Vercel (two projects), Neo4j Aura, Groq, Google Gemini |
docs/frontend/— the frontend's architecture, file structure, API integration, state management, component hierarchy, and milestone plan.IMPLEMENTATION_PLAN.mdcarries as-built notes per milestone and is the closest thing to a build log; the structural documents predate the auth slice and some later refactors.
The three READMEs are the current state of the project. A few source comments
still cite backend design documents (a Graph RAG plan, a graph-recall metric
design, a 2026-07-18 repository analysis) that are no longer part of the
repository; the decisions they were cited for are summarised in
backend/README.md instead.
cd backend && uv run pytest -q # Neo4j must be running for the integration tests
cd frontend && npm run test # Vitest + MSW, no backend requiredThe backend suite is 105 tests; 42 of them — the Pydantic models, the whole
graph-recall metric and its dataset validation, the AI stub-provider test, the
canonical-text tests, and two others — need nothing but Python. The remaining
63 execute real Cypher against the configured Neo4j instance. Each of those
registers its own account and runs inside that account's world, so tests are
isolated from each other by the same ownership rule that isolates users, and
each one deletes its accounts and everything they own afterwards — a suite run
leaves the database exactly as it found it. RAG tests never call a real
Ollama/Groq/Google — tests/conftest.py overrides the embedding provider with
a deterministic fake for the whole suite, and the chat provider is stubbed per
test — and test_rag_isolation.py proves one account's retrieval can never
surface another's entity, even a byte-identical one, which is the regression
test for the multi-tenancy trap in db.index.vector.queryNodes() (explained
in full under
the two Graph RAG decisions
in the backend README).
Retrieval quality is a separate, measured question that pytest deliberately
does not answer — the fake embedder has no semantics at all. That's what
backend/scripts/evaluate_graph_recall.py and the graph-recall metric are for;
see Real Embedding Evaluation.
The frontend suite is 372 tests across 26 files, all pure TypeScript, with the backend mocked at the network boundary: the API/schema spine, the four entity schemas, the domain rules, the graph's pure layers, and the AI slice's resource and citation parsing. It has no component tests yet.
The backend enforces one-directional dependencies — api → services → repositories → db, with core, domain, and providers as leaves. All
Cypher lives in repositories/. The frontend is feature-sliced: app/
(composition root and shell), routes/ (URL map), features/ (vertical
slices), shared/ (design system, entity engine, API core). Each README
explains its own half.