Skip to content
ImanolGoPublic

About

πŸ•΅οΈ A murder mystery where every suspect is an LLM agent that remembers, schemes and lies. Multi-agent LangGraph, MCP tools, RAG memory, simulation-based evals in CI, and a fine-tuned lie detector.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

47 Commits

Folders and files

Repository files navigation

Alibi β€” a murder mystery where every suspect is an LLM

Alibi

A small murder mystery where every suspect is an LLM agent that remembers, keeps secrets and lies. You are the detective.

Alibi is a learning project: one clean, working example of each core LLM-engineering concept (structured outputs, RAG, MCP, agent orchestration, evals, fine-tuning, observability) in a codebase small enough to read. The roadmap and milestone checklists live in docs/PLAN.md.

Status

Milestone State
M0 Skeleton βœ…
M1 The Case File 🚧 (10/10 cases generate; paper-solving pending)
M2 One Suspect βœ…
M3 World + MCP βœ…
M4 Evals βœ… (20-game run, gates, 90% labeller agreement)
M5 Detector + Web + Homelab 🚧 (code, web UI + deploy done; fine-tune, demo GIF and docker-stats pending)

Quickstart

make install                 # uv sync
make up                      # postgres + phoenix (docker)
cp .env.example .env         # add your OPENROUTER_API_KEY
make hello                   # one real LLM call: tokens, cost, latency (traced)

make case                    # generate + validate a case into cases/
uv run alibi interrogate --case cases/<id>.json --suspect <id> --debug
uv run alibi play --case cases/<id>.json --new
make test && make lint

Demo

The board: suspects and evidence as typed index cards on green baize, with the story clock and lie-detector charges

The same board on a phone: one column with the cards stacked

The reveal: your detective rank, what you missed, every secret and the true timeline

Screenshots use the test case with scripted answers. A recorded GIF of a real game and a live link are still to do.

Architecture

            Browser (HTMX)      Terminal (Rich)     Any MCP client
                  β”‚                   β”‚                   β”‚
                  └─────── FastAPI / CLI ──────┐          β”‚
                                               β–Ό          β–Ό
                                     MCP server (game tools)
                                               β”‚
                              Game loop (LangGraph): actions, turns, score
                                               β”‚
                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                       β–Ό                       β–Ό                     β–Ό
               Suspect agent (LangGraph)   Truth labeller      Lie detector
               retrieve β†’ think β†’ speak    claims β†’ TRUE/FALSE (ONNX, CPU, M5)
               β†’ remember                        β”‚
                       β”‚                         β–Ό
                 pgvector memory          stored labels β†’ evals + training data
                       β”‚
                       β–Ό
     llm.py (LiteLLM) ── hosted models, cost/latency/tokens, daily spend cap
                       β”‚
     OpenTelemetry ── Phoenix

     Offline (laptop / CI): case generator + validator, solver agent, evals,
                            lie-detector training (Colab) β†’ model.onnx

The stack, and why

Every choice is here to teach one thing. The "why" is the point.

Concern Choice Why
Language / packaging Python 3.12 + uv uv replaces pip/venv/pyenv wheel; one lockfile, fast installs, reproducible builds.
Schemas Pydantic v2 A schema is the contract for structured LLM output; parsing and validation come for free.
LLM access LiteLLM, behind llm.py One interface over many providers (openrouter/…, openai/…); swap models in config/models.yaml, no code change. Also gives us token/cost accounting.
Agent orchestration LangGraph Agents as explicit state machines (retrieve β†’ think β†’ speak β†’ remember). Edges and nodes make the loop inspectable and testable, unlike a hidden ReAct string loop.
Tools MCP Python SDK A standard tool protocol: the game is exposed once and used by the terminal, the solver, and external clients alike. Tool descriptions double as prompts.
Memory Postgres 16 + pgvector One database for both durable game state and vector search β€” no separate vector store to run on a 4 GB box.
DB access SQLAlchemy 2.x, create_all Typed models and queries; migrations (Alembic) are overkill for a project this size.
Observability OpenTelemetry β†’ Arize Phoenix Vendor-neutral spans for every node and LLM call, with cost/latency attached. Tracing is wired in from M0 and used everywhere.
API / UI FastAPI + Jinja + HTMX Server-rendered pages, no JS build step (M5).
CLI Typer + Rich Type-hint-driven commands; readable terminal output.
ML (training only) PyTorch + HF, export to ONNX Training happens on Colab/Kaggle; the server only runs ONNX Runtime on CPU β€” so torch never touches the homelab.
Infra Docker Compose + Makefile + GitHub Actions Same compose for dev and homelab; CI runs lint, tests and (for prompt changes) an eval gate.

How it works

Each milestone adds one concept; see docs/results/ for the measured outcomes.

  1. M1 β€” structured output + validation. One LLM call fills a Pydantic Case from config/setting.yaml; a deterministic validator checks eight rules and a bounded repair loop feeds violations back to the model. Creativity is the model's job; correctness is the code's. (case.py, generator.py, validator.py)
  2. M2 β€” RAG memory + a suspect agent. Each suspect stores memories in pgvector and answers through a LangGraph. Information hiding is a code guarantee: the prompt only ever sees the persona, the suspect's own secrets and what knowledge.py says they know. (knowledge.py, memory.py, suspect.py)
  3. M3 β€” the world and MCP. A Game owns the rules (20 actions, scoring, persistence); the terminal and an MCP server are thin adapters over it. State lives in Postgres, so a game survives a restart. (game.py, mcp_server.py)
  4. M4 β€” evals. An automatic detective plays through the tools; a judge labels every claim against the ground truth; metrics gate changes in CI. (solver.py, truth.py, evals.py, config/gates.yaml)
  5. M5 β€” detector, web UI, homelab. A tiny DistilBERT fine-tuned on our own TRUE/FALSE claims, exported to ONNX, plus a browser UI and the deployment below.

Results

Measured numbers (full write-ups in docs/results/).

Game quality β€” 20-game eval (uv run alibi eval --games 20 --gate):

Metric Value Gate
case_valid_rate 1.000 β‰₯ 0.80
solve_rate 0.850 0.40–0.85
contradiction_rate 0.029 < 0.08
early_confession_rate 0.000 < 0.10
cost_per_game $0.030 ≀ $0.50

Honest caveat: the raw solve_rate counts timed-out games as losses; of the games it finished, the auto-solver won 17/17. A capable model finds a four-suspect mystery too easy β€” the number that matters for a game is the human one, which the browser UI provides. See docs/results/m4.md.

Lie detector vs baselines (one statement, no case context; 30-statement sample):

Model F1 (lie) Latency Cost / prediction
always-TRUE 0.000 β€” β€”
majority 0.000 β€” β€”
LLM judge 0.182 7.7 s $0.00022
fine-tuned ONNX 0.333 40 ms ~$0

Baselines: uv run alibi baselines --data ml/data/test.jsonl. Details and the training run live in ml/MODEL_CARD.md.

Deploy (Docker / homelab)

The app is one small image (python:3.12-slim, no torch), run twice: as MCP over streamable HTTP (port 8080) and as the browser game (port 8000), alongside Postgres and Phoenix β€” sized to stay under ~2 GB on an Intel N150 with no GPU.

cp .env.example .env                      # set OPENROUTER_API_KEY (+ low budget)
docker compose -f compose.homelab.yml run --rm app alibi case --seed 1
make deploy                               # docker compose -f compose.homelab.yml up -d --build

Or build/run the image directly:

docker build -t alibi .
docker run --rm -p 8080:8080 --env-file .env -v alibi_cases:/app/cases alibi
# the browser game (same image, different command):
docker run --rm -p 8000:8000 --env-file .env -v alibi_cases:/app/cases alibi \
  alibi web --host 0.0.0.0 --port 8000

Full walkthrough β€” Cloudflare Tunnel (public access, no open ports), cost caps, memory limits and Postgres backup β€” is in docs/deploy-homelab.md.

Play from an MCP client

The whole game is exposed as MCP tools. Point any client at the stdio server:

{
  "mcpServers": {
    "alibi": {
      "command": "uv",
      "args": ["run", "alibi", "mcp", "--case", "cases/<id>.json"],
      "cwd": "/absolute/path/to/alibi"
    }
  }
}

Or at the deployed HTTP endpoint: { "type": "http", "url": "http://<vm>:8080/mcp" }.

Tools: case_summary, status, list_rooms, search_room, inspect, question, lie_detector, accuse.

Play in the browser

uv run alibi web        # http://127.0.0.1:8000

FastAPI + Jinja + HTMX, no JS build step (htmx and the fonts are served from src/alibi/static, so no CDN is needed). Generate a case (or pick one), search rooms, question suspects β€” presenting evidence when you have it β€” test an answer with the lie detector, then accuse and read "what really happened".

Every "Start a game" is a fresh game with its own URL, so a case can be replayed and visitors of a public deploy never share one. The story clock turns your 20 actions into a night: you arrive an hour after the death, each action takes a quarter of an hour, and the murderer leaves when the roads clear at dawn. The reveal page ranks you (Constable to Great Detective) and lists the real clues you missed. The same Game object backs the web, CLI and MCP server.

Evals

make eval                          # uv run alibi eval --games 20 --gate
uv run alibi eval --games 5 --seed 1

Metrics (config/gates.yaml): case_valid_rate, solve_rate, contradiction_rate, early_confession_rate, cost_per_game. A GitHub Action runs a 5-game gate on PRs touching src/alibi/prompts/** or suspect.py and comments the summary on the PR. The gate is exercised by deliberately breaking a suspect prompt: the closed "broken prompt" PR.

alibi export-dataset turns the stored labels into fine-tuning JSONL (split by case, no leakage); alibi baselines scores always-TRUE, majority and the LLM judge for comparison with the fine-tuned detector.

Layout

src/alibi/        case Β· generator Β· validator Β· knowledge Β· llm Β· memory
                  suspect Β· game Β· mcp_server Β· solver Β· truth Β· evals
                  detector Β· export Β· baselines Β· web Β· templates Β· static Β· cli
config/           models.yaml (role β†’ model) Β· setting.yaml Β· gates.yaml
tests/            deterministic tests only (no network; FakeLLM)
ml/               dataset export + training (M5; never installed on the server)
assets/           banner + demo screenshots
docs/PLAN.md      roadmap Β· docs/adr/ decisions Β· docs/results/ measurements
Dockerfile        small runtime image        compose.homelab.yml  the deploy

Rules of the house

  • Every LLM/embedding call goes through src/alibi/llm.py, with a role.
  • Models come from config/models.yaml; prompts from src/alibi/prompts/*.jinja.
  • The validator, not the model, decides whether a case is sound.

License

MIT β€” see LICENSE.

About

πŸ•΅οΈ A murder mystery where every suspect is an LLM agent that remembers, schemes and lies. Multi-agent LangGraph, MCP tools, RAG memory, simulation-based evals in CI, and a fine-tuned lie detector.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages