An LLM-powered, ontology-based knowledge wiki. Raw sources are ingested, synthesized by an LLM into two linked layers, reviewed by a human, then served to other LLMs through a Model Context Protocol (MCP) server and a REST API.
- Page level — each source is summarized into a Page. Pages link to related pages (
page → related pages). - Term / concept level — an Ontology of concepts (nodes) and typed relationships between them (
term → relation → term).
Add source → Two-step LLM synthesis → Human review/approve → Serve via API + MCP
ingest analyze → generate review queue pages / ontology / hybrid search / MCP
Synthesis runs in two LLM passes: an analysis pass (entities, concepts, connections, contradictions, structure plan) followed by a generation pass that writes the page + ontology from that analysis. This produces more consistent structure than a single pass.
Everything produced by the LLM lands in a review queue with status pending. Nothing is public until a human approves it. Approved pages and concepts are embedded and projected into the graph for retrieval.
Search is hybrid: pgvector semantic similarity + lexical (trigram/ILIKE) + Neo4j graph expansion, fused with Reciprocal Rank Fusion (RRF). This scales to large corpora (HNSW ANN index) and answers both single-hop ("what's similar") and multi-hop ("what connects to X") queries. Exposed at GET /search and via the MCP hybrid_search tool.
Login is via generic OpenID Connect, so it works with PSU SSO, Keycloak, Auth0, Azure AD, Google, etc. The backend runs the OAuth flow (Authlib), then issues a JWT in an HttpOnly cookie; protected endpoints depend on require_user. Access is restricted by an allowlist (AUTH_ALLOWED_EMAILS / AUTH_ALLOWED_DOMAINS).
GET /auth/login → redirect to the OIDC provider
GET /auth/callback→ exchange code, check allowlist, set cookie, redirect to frontend
GET /auth/me → current user POST /auth/logout → clear cookie
To enable it, set AUTH_ENABLED=true and fill the OIDC_* values. For PSU SSO, point OIDC_DISCOVERY_URL at the provider's .well-known/openid-configuration (or set the three endpoints manually) and set AUTH_ALLOWED_DOMAINS=psu.ac.th. With AUTH_ENABLED=false (default) the app runs open for local development. The MCP server is a separate trusted process and is not behind this login.
Nothing about any subject is hardcoded. An optional domain profile (DOMAIN_PROFILE_PATH) injects purpose/scope hints into the prompts; with none set the system runs as a neutral general-purpose ontology builder. Swap the profile to target any corpus. See backend/domain_profiles/.
| Layer | Tech |
|---|---|
| Backend | FastAPI (Python) |
| Relational | PostgreSQL + pgvector (rows + embeddings) |
| Graph | Neo4j (ontology + page links) |
| LLM + Embed | OpenAI-compatible API (configurable base URL) |
| Retrieval | Hybrid: vector + lexical + graph, fused via RRF |
| MCP | mcp Python SDK (stdio + HTTP) |
| Frontend | Next.js (App Router, CSR only) |
# 1. Infra
cp .env.example .env # fill in EMBEDDING_API_KEY etc. (LLM is configured per project in the UI)
docker compose up -d postgres neo4j
# 2. Backend
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
alembic upgrade head # create schema via migrations (recommended)
uvicorn app.main:app --reload # http://localhost:8000/docs
# 3. MCP server (separate process)
cd backend
fastmcp run mcp_server.py # stdio MCP for LLM clients
# 4. Frontend
cd frontend
npm install
npm run dev # http://localhost:3000Postgres schema is managed by Alembic (backend/migrations/). The initial migration creates every table, the vector + pg_trgm extensions, and the HNSW/trigram indexes.
cd backend
alembic upgrade head # apply all migrations
alembic revision --autogenerate -m "message" # create a new migration after model changes
alembic downgrade -1 # roll back one
alembic upgrade head --sql # preview SQL without touching the DBThe DB URL and target metadata come from app.config, so migrations stay in sync with the models. For quick local dev you can instead set AUTO_CREATE_TABLES=true to let the app create tables on startup; in production keep it false and rely on Alembic (docker-compose already runs alembic upgrade head before the API starts).
See ARCHITECTURE.md for the full design and data model.