Multi-Agent Research Assistant
A self-hostable, bring-your-own-key research assistant with an auditable
human-in-the-loop approval gate and verifiable per-claim citations.
Apple Silicon macOS desktop builds are on the download page; Windows and Linux are offered the last build published for them, which predates the corpus features. See the desktop installation guide for per-platform setup notes.
Ask a research question. A pipeline of specialized agents searches the web and your uploaded documents, gathers evidence with sources, drafts a cited report, and pauses for your approval before finalizing.
Research that shows its work. The evidence, sources, claims, claim-evidence links, conflicting sources, and your review decisions are all structured records you can inspect, and the report is a rendering of them. Three distinctions are enforced everywhere and never blurred: retrieved is not verified, retrieved is not cited, and a citation marker is not evidence. Approving a report freezes an artifact that anyone can check offline with the verifier in this repository — no network, no model, no account required. See the research model.
Approve it, or send it back with feedback. Completed reports support grounded follow-up
chat and export as .md, .pdf, or a hash-verifiable .bundle.json that a third party
can check offline — no AI, no network, no account.
Verify an artifact yourself, without this application running:
python -m research_engine.verify_bundle research-abc12345.bundle.json
Six checks — bundle integrity, report integrity, evidence integrity, citation resolution,
claim/evidence linkage and the approval chain. Exit 0 when they all pass. A pass means the
artifact is internally consistent and unaltered since approval; it does not mean the research
is correct, which is a judgement no checker can make. A bundle also records the prompt every
agent ran on, and the verifier names any agent whose instructions were replaced.
Every claim carries an inline [n] citation that resolves to a real source with the
verbatim supporting snippet. A citation that doesn't resolve renders as a visible ⚠
"unverified" chip — the system surfaces its own failures instead of hiding them.
Features
- Verifiable citations — every
[n] resolves to a source and the verbatim snippet behind
it; one that does not renders a ⚠ chip instead of rendering clean.
- Two human gates — review the plan before any search spends money, and the draft before
anything is final.
- A research record, not just a report — evidence, sources, claims, claim → evidence
links and conflicting sources are records you can inspect, and reports are versioned.
- Offline verification — approving freezes an artifact whose
.bundle.json checks with
no network, no model and no account.
- Your instructions for each agent — replace how the planner, executor, critic,
synthesizer and follow-up chat behave. The checks that keep research honest cannot be
rewritten, and every run records the instructions it ran on.
(Agent instructions)
- Any model, per agent — Anthropic, Google, OpenAI, OpenRouter, any OpenAI-compatible
endpoint, or local models through Ollama, on your own keys.
- Your own documents — a corpus per project, and corpus-only research: evidence comes
only from your uploaded documents, web search is off, and indexing requires a local model.
The question and the excerpts still go to whichever provider each research model
(planner, executor, critic, synthesizer) is set to use — a cloud provider unless you have
chosen local models. A local indexing model does not make the run offline.
- Project memory and chat — project chat answers from the reports you approved and cites
them (self-hosted server).
- Desktop app or self-hosted — an Apple Silicon macOS app (Windows and Linux: the last
build published for them) with no Docker, database or login to set up, or the full stack
with one Docker command.
- Measured, with its caveats — the 3.0.0 release run measured 96.4% citation support over
ten fixed questions, graded by a judge that is none of the models under test.
(What was measured)
How it works
The pipeline is a compiled LangGraph
StateGraph. Each agent has one job, and the graph's conditional edges enforce budgets
and retries.
┌─── rework ────┐
▼ │
Query → Planner → ⏸ DESIGN GATE → Executor ⇄ Critic → Synthesizer → ⏸ REVIEW GATE → Artifact
(drop tasks) (tools) (fail- (cited (approve / (verifiable
closed) draft) rework) export)
- Planner decomposes the question into research tasks. Depth (
fast / balanced /
comprehensive) sets how many, and is therefore the main cost dial.
- Design gate pauses before anything is searched, so you see the research areas the
planner chose — and drop the ones you did not ask for — while the run is still free. A
dropped task is never researched. This is the difference between "the agent picked six
queries" and "these are the ones I asked for".
- Executor runs real tool calls (
web_search, read_webpage) and returns structured
evidence.
- Critic grades that evidence and fails closed — invalid or missing critic output
counts as a failure, never a pass — sending weak tasks back within a bounded retry limit.
- Review gate is a real
interrupt() checkpoint, not a polling loop. State is durably
checkpointed and the worker exits; a rework resumes at the synthesizer with the same
evidence rather than re-running research you already paid for.
- Approval freezes the report, its evidence and your decision into a verifiable artifact.
Every
[n] is a chip you can hover for the source title, domain, and the verbatim snippet
supporting that claim.
Self-host — one command
Prerequisites: Docker and Docker Compose.
./start.sh
The script creates .env if missing (generating a JWT secret), checks your config, builds
and starts all five services, waits until every one reports healthy, and opens the app.
./start.sh --logs # start, then follow logs
./start.sh --stop # stop (data preserved)
./start.sh --reset # stop and delete all data (asks first)
Or drive compose yourself:
cp .env.example .env
# In .env set: JWT_SECRET_KEY=$(openssl rand -hex 32) and a provider key.
docker compose -f docker-compose.full.yml up --build
Open http://localhost:3031 → register → ask a question → watch the pipeline → approve
→ read the cited report.
The API container runs alembic upgrade head before serving, and the worker and frontend
wait on its readiness, so migrations apply exactly once. The frontend is the only
published service — it proxies /api/* internally, so auth cookies stay first-party and
the database is never exposed.
Native development
make infra-up # Postgres + Redis only
make backend-setup && make migrate
make backend-dev # API → :8000
make worker # Celery worker (new terminal)
make frontend-setup && make frontend-dev # UI → :3031 (new terminal)
make test / test-backend / test-frontend # pytest + Vitest
make eval # report-quality eval suite
make lint / format
Bring your own key
Users can paste their own provider key and run research on their own account. The key is
encrypted at rest (Fernet/AES), never returned by any endpoint, and never sent back to
the browser — the UI shows only the last four characters.
Supported providers: Anthropic, Google, OpenAI, OpenRouter, plus any
OpenAI-compatible endpoint (custom:) and local models via Ollama. A user without a
key falls back to the deployment's shared server key, subject to their monthly token limit.
For a public deployment, require BYOK from everyone and leave the server key unset:
ENVIRONMENT=production # secure cookies, /docs disabled
DEFAULT_MONTHLY_TOKEN_LIMIT=50000 # applied to every new account
ENCRYPTION_KEY=<openssl rand -hex 32> # encrypts users' stored keys
ENCRYPTION_KEY is what protects your users' API keys. Unset, it derives from
JWT_SECRET_KEY — which works, but rotating your JWT secret then makes every stored key
undecryptable. Set it explicitly in production so the two rotate independently.
Architecture
- Backend — FastAPI + Celery worker, PostgreSQL 16 (pgvector), Redis 7
- Agents — LangGraph
StateGraph with Postgres checkpointing; HITL via interrupt();
structured Pydantic outputs; fail-closed critic; per-session cost and wall-clock budgets
- LLMs — provider-pluggable via
MODEL_* routing (provider:model), per agent role
- Search — Tavily → Brave → DuckDuckGo fallback, Redis-cached, with an SSRF guard on
page fetching and untrusted-content framing around everything the web returns
- Frontend — Next.js 16 (App Router), Tailwind v4, TanStack Query; same-origin
/api
proxy so cookies stay first-party and SSE authenticates natively
- Security — httpOnly cookie auth with rotating refresh tokens and reuse detection,
per-operation atomic rate limits, encrypted BYOK keys, strict CSP
(Security)
Configuration
Every variable is documented in .env.example and validated at startup —
the app refuses to boot with a placeholder or short JWT_SECRET_KEY.
| Variable | Required | Notes |
|---|
JWT_SECRET_KEY | ✅ | ≥ 32 random chars (openssl rand -hex 32) |
DATABASE_URL, REDIS_URL | ✅ | Set for you by the full-stack compose |
MODEL_PLANNER … MODEL_CHAT | ⚪ | provider:model per agent role |
ENCRYPTION_KEY | ⚪ (prod) | Encrypts users' stored keys |
DEFAULT_MONTHLY_TOKEN_LIMIT | ⚪ | Token cap for new accounts; 0 = unlimited |
MAX_COST_PER_SESSION_USD | ⚪ | Per-session budget; 0 = unlimited, and 0 is the default |
ENVIRONMENT | ⚪ | development or production |
Every run limit is 0 = unlimited, and 0 is the default — cost, wall-clock, and
input tokens alike. Nothing stops a long run out of the box; set them when you want a hard
stop. And MAX_COST_PER_SESSION_USD is a no-op on openrouter and custom routes —
those providers are skipped by the price catalog, so estimated cost is 0.00 and the cap
never trips. Cap spend at the provider for those.
Full list with exact defaults: Configuration reference.
Deployment
Single host plus a TLS reverse proxy in front of the frontend
(Deployment):
Tagging vX.Y.Z triggers release.yml, which builds
multi-arch (amd64 + arm64) api/worker/frontend images to GHCR and cuts a GitHub
Release; desktop.yml builds and attaches the desktop
bundles. Every release follows RELEASE.md, and its evidence is kept in
release-audit/.
Documentation
Read it online at
adityamhaske.github.io/Multi-Agent-Research-Assistant/docs,
or in this repository under docs/.
New here: Overview →
Quick start →
Configuration
License
MIT — free to use, modify, and self-host, commercially or otherwise.