Multi-agent research report generation system with human-in-the-loop outline review, parallel fact-checking, and PDF export.
ResearchForge generates citation-backed research reports through a six-agent
LangGraph pipeline. A user submits a topic; the system performs live web
research (Tavily), processes any uploaded PDFs, fan-outs claim verification
across the corpus in parallel via the LangGraph Send() API, drafts an
outline, then pauses for human approval before writing. After outline
approval, the synthesis agent writes one section at a time, the citation
agent deduplicates and numbers references, and the system emits a final
PDF via ReportLab. All session state is persisted to Postgres so the
backend is safe to restart mid-run.
┌─────────────────────────────────────────────────────┐
│ Supervisor (routes) │
└─────────────────────────────────────────────────────┘
│
┌─────────────┬─────────────┬───────┴───────┬─────────────┬─────────────┐
▼ ▼ ▼ ▼ ▼ ▼
Research Document FactCheck Outline Synthesis Citations
(Tavily) (PDF text) (Send-fanout) (HITL gate) (per-section) (dedupe)
│
▼
N parallel claim
verifications, then
merge → supervisor
User → Research → Document → FactCheck (parallel) → Outline →
[interrupt: human approves outline] →
Synthesis (per-section) → Citations → PDF Export
| Agent | Responsibility | LLM |
|---|---|---|
| Research | Live web search via Tavily; collects source documents | — (tool only) |
| Document | Extracts and summarizes text from uploaded PDFs | GPT-4o |
| FactCheck | Verifies extracted claims in parallel against the source set | GPT-4o |
| Outline | Drafts section structure; graph pauses for human approval | GPT-4o |
| Synthesis | Writes each section with inline source references | GPT-4o |
| Citations | Deduplicates URLs across sections; assigns numbered refs | — (rule-based) |
.
├── backend/
│ ├── Dockerfile Multi-stage build, non-root user
│ ├── server.py FastAPI app, SSE streaming, rate limiting
│ ├── db.py Postgres session store + connection pool
│ ├── requirements.txt Human-edited top-level deps
│ ├── requirements.lock Pinned transitive deps for reproducible builds
│ ├── graph/
│ │ ├── graph.py LangGraph compilation; Send() fan-out wiring
│ │ ├── supervisor.py Routes between agents
│ │ ├── state.py ReportState TypedDict
│ │ └── agents/ Six agent node modules (one per pipeline step)
│ ├── utils/ Shared OpenAI/Tavily client singletons,
│ │ URL validation, scoring helpers
│ ├── eval/langsmith_tracer.py LangSmith trace setup
│ └── export/pdf_exporter.py ReportLab PDF generation
├── frontend/
│ ├── Dockerfile Two-stage: yarn build → nginx
│ ├── nginx.conf SPA routing + SSE-friendly /api proxy
│ ├── vercel.json Production rewrites → Railway backend
│ ├── package.json React 19 + Zustand + Tailwind + craco
│ └── src/ App, store, components
├── docker-compose.yml Local-dev Postgres (only)
├── railway.toml Railway backend deploy config
├── .ruff.toml Lint + format rules
└── README.md
ResearchForge runs the backend and frontend directly on the host. Docker is used only to provide Postgres.
- Python 3.11+
- Node.js 20+ with Yarn
- Docker Desktop (for the Postgres container)
- An OpenAI API key and a Tavily API key
git clone <your-repo-url>
cd Research_Forge
cp backend/.env.example backend/.env
# Edit backend/.env and fill in OPENAI_API_KEY and TAVILY_API_KEYdocker-compose up -dThis launches researchforge-postgres on localhost:5432 with the
credentials baked into docker-compose.yml. The default
DATABASE_URL in backend/.env.example matches.
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn server:app --reload --port 8000The server validates required env vars on startup and creates the
sessions table on first connection. Health: http://localhost:8000/api/health.
cd frontend
yarn install
yarn startThe CRA dev server runs on http://localhost:3000 and proxies /api/*
to localhost:8000 via the proxy field in package.json.
Visit http://localhost:3000.
All variables below live in backend/.env. The frontend needs none in
either local dev (handled by CRA proxy) or production (handled by
vercel.json rewrites).
| Variable | Required | Default | Description |
|---|---|---|---|
OPENAI_API_KEY |
Yes | — | Used by every agent that calls GPT-4o (Document, FactCheck, Outline, Synthesis). |
TAVILY_API_KEY |
Yes | — | Used by the Research agent for live web search. |
DATABASE_URL |
Yes | — | Postgres DSN. Persists sessions and LangGraph checkpoints. Railway injects this when a Postgres addon is provisioned. |
CORS_ORIGINS |
Yes | — | Comma-separated list of allowed frontend origins. Use the Vercel deployment URL in production. |
LANGCHAIN_TRACING_V2 |
No | false |
Enables LangSmith tracing of every LLM call and graph node. |
LANGCHAIN_API_KEY |
No | — | Required if LANGCHAIN_TRACING_V2=true. |
LANGCHAIN_PROJECT |
No | ResearchForge |
LangSmith project name; traces are bucketed under it. |
LANGCHAIN_ENDPOINT |
No | LangSmith default | Override only if using a self-hosted LangSmith instance. |
- Push the repo to GitHub.
- Create a new Railway project and connect the GitHub repo.
- Add the Postgres addon. Railway injects
DATABASE_URLautomatically into the backend service. - In Variables, set:
OPENAI_API_KEYTAVILY_API_KEYCORS_ORIGINS— e.g.https://your-app.vercel.app- LangSmith vars (optional):
LANGCHAIN_TRACING_V2,LANGCHAIN_API_KEY,LANGCHAIN_PROJECT
- Railway detects
railway.tomlat the repo root and builds the backend frombackend/Dockerfile. No build command is needed. - The deploy section in
railway.tomlconfigures/api/healthas the health-check path. Railway will mark the deploy healthy only when that endpoint returns 200.
- Edit
frontend/vercel.jsonand replace thedestinationURL with your Railway backend URL (https://<service>.up.railway.app/api/:path*). - Connect the GitHub repo to Vercel.
- In Vercel project settings, set Root Directory to
frontend/. - Deploy. No env vars are required —
vercel.jsonrewrites handle API routing. - After deploy, copy the Vercel URL into the Railway
CORS_ORIGINSvariable so the backend accepts requests from it.
# Lint
ruff check backend/
# Format
ruff format backend/Configuration lives at .ruff.toml at the repo root.
- Session persistence: Sessions and LangGraph checkpoints live in
Postgres (
sessionstable +langgraph-checkpoint-postgresschema). The backend is safe to restart mid-run — in-flight runs resume from the last checkpoint after the next user action. - Uploaded PDFs: stored at
/tmp/researchforge_uploads/. Removed when the owning session expires. - Generated PDFs: stored at
/tmp/researchforge_pdfs/. Removed when the owning session expires. - Session TTL: 2 hours (
SESSION_TTL_SECONDS = 7200inserver.py). The cleanup loop runs every 30 minutes. - LangSmith traces: set
LANGCHAIN_TRACING_V2=trueto capture every LLM call, every graph node, and Tavily searches. Trace URL is returned in the/api/runresponse when tracing is enabled. - Rate limits:
/api/runis capped at 10/min per IP;/api/upload-pdfis capped at 30/min per IP (via SlowAPI). - Graph execution timeout: 600 seconds per run
(
GRAPH_EXECUTION_TIMEOUTinserver.py). Long runs hit this and are marked errored. - SSE streaming:
/api/session/{id}/streamemits progress events. The nginx config infrontend/nginx.confdisables proxy buffering so events reach the browser in real time. - Outbound proxy hygiene:
server.pystripsHTTP_PROXY/HTTPS_PROXYenv vars at startup because Tavily and OpenAI need direct outbound connections. If you must run behind a proxy, add the relevant domains toNO_PROXY.
| Layer | Technology | Version |
|---|---|---|
| LLM Orchestration | LangGraph | >=1.0 |
| LLM Provider | OpenAI GPT-4o | — |
| Web Research | Tavily | — |
| Backend | FastAPI + uvicorn | >=0.115 |
| Database | PostgreSQL + psycopg3 | 16 |
| Checkpointing | langgraph-checkpoint-postgres | >=2.0 |
| Frontend | React 19 + Zustand | — |
| PDF Export | ReportLab | >=4.0 |
| Tracing | LangSmith | optional |