A production-grade, objective-driven AI research tool. Give it a company and a free-form goal — "evaluate as an acquisition target", "prepare for a partnership call", "assess as a vendor" — and it runs a multi-step LangGraph workflow that produces a structured briefing with real sources, plus a grounded follow-up chat interface. The objective isn't a label; it steers every LLM prompt so a partnership report and an acquisition report are materially different documents.
┌─────────────┐ ┌──────────────────────────────────────────────────┐
│ Browser │◄──SSE────│ FastAPI (port 8000) │
│ React/Vite │──REST───►│ /api/sessions /api/sessions/:id/run /api/chat │
└─────────────┘ └───────────────────────┬──────────────────────────┘
│ invoke / astream_events
┌─────────────▼──────────────┐
│ LangGraph Workflow │
│ │
│ planner ──► researcher │
│ ▲ │ │
│ │ analyst │
│ │ │ │
│ │ quality_check │
│ │ / \ │
│ └─(fail) (pass) │
│ report_gen │
└────────────────────────────-┘
│
┌─────────────▼──────────────┐
│ PostgreSQL (+ checkpointer)│
│ Sessions · Messages · State│
└────────────────────────────-┘
flowchart LR
A([planner]) --> B([researcher])
B --> C([analyst])
C --> D([quality_check])
D -- "pass OR max_revisions" --> E([report_generator])
D -- "fail + revisions left" --> B
E --> F([END])
Each node receives the full ResearchState and returns a partial dict; LangGraph merges them. The objective text is passed verbatim into every node prompt so the graph is fully domain-agnostic.
Prerequisites: Docker Desktop, an OpenAI key, a Tavily key.
# 1. Clone and enter the repo
git clone <repo-url> && cd ai-research-copilot
# 2. Create your .env from the example
cp .env.example .env
# → edit .env: set OPENAI_API_KEY and TAVILY_API_KEY
# 3. Start everything
docker compose up --build
# 4. Open the app
open http://localhost:3000 # frontend
# API health check
curl http://localhost:8000/api/healthThat's it. The compose file starts PostgreSQL, waits for it to be healthy, then starts the API and frontend.
cd backend
pip install -r requirements.txt
# Configure
cp ../.env.example .env
# Edit .env: set OPENAI_API_KEY, TAVILY_API_KEY
# DATABASE_URL defaults to SQLite — no extra setup needed
uvicorn app.main:app --reload --port 8000cd frontend
npm install
npm run dev # → http://localhost:5173 (proxies /api to port 8000)cd backend
pytest tests/ -vTests inject stub env vars via conftest.py and compile the graph without a checkpointer — no database required.
cd backend
python -m app.graph.run "Stripe" "https://stripe.com" "evaluate as a payments partner"| Variable | Required | Default | Description |
|---|---|---|---|
OPENAI_API_KEY |
Yes | — | OpenAI API key |
TAVILY_API_KEY |
Yes | — | Tavily search API key |
DATABASE_URL |
No | sqlite+aiosqlite:///./research_copilot.db |
SQLAlchemy async URL |
MODEL_NAME |
No | gpt-4o |
OpenAI model (swappable) |
MAX_REVISIONS |
No | 2 |
Quality-check retry cap |
POSTGRES_USER |
Compose only | copilot |
Postgres user (docker-compose) |
POSTGRES_PASSWORD |
Compose only | copilot |
Postgres password (docker-compose) |
POSTGRES_DB |
Compose only | research_copilot |
Postgres database (docker-compose) |
SQLite note:
db/session.pyauto-convertssqlite:///→sqlite+aiosqlite:///for the async driver. The LangGraph SQLite checkpointer writes to a separate file (research_copilot_checkpoints.db).
All routes under /api:
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check |
POST |
/api/sessions |
Create session (company_name, website, objective) |
GET |
/api/sessions |
List sessions |
GET |
/api/sessions/{id} |
Session detail + stored report |
POST |
/api/sessions/{id}/run |
SSE stream — run workflow, emits node events |
POST |
/api/sessions/{id}/chat |
Grounded chat over the report |
GET |
/api/sessions/{id}/messages |
Chat history |
ai-research-copilot/
├── backend/
│ ├── app/
│ │ ├── graph/ # LangGraph: state, nodes, builder, schemas, CLI runner
│ │ ├── api/ # FastAPI routers: sessions, workflow SSE, chat, health
│ │ ├── db/ # SQLAlchemy async models, session factory, init
│ │ ├── services/ # Tavily search + website fetch with tenacity retry
│ │ ├── config.py # pydantic-settings (reads .env)
│ │ └── main.py # FastAPI app + lifespan
│ ├── tests/
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── api/ # Typed API client + TypeScript types
│ │ ├── components/ # ReportView, ChatPanel, ProgressTimeline, Layout, Sidebar
│ │ ├── hooks/ # useRunSession (SSE / fetch ReadableStream consumer)
│ │ └── pages/ # SessionCreate, SessionDetail
│ ├── nginx.conf
│ └── Dockerfile
├── docs/
│ ├── architecture.md
│ ├── engineering-decisions.md
│ └── product-improvements.md
├── docker-compose.yml
├── .env.example
└── README.md
docs/architecture.md— deep-dive on components, data flow, streaming, checkpointingdocs/engineering-decisions.md— non-obvious choices, tradeoffs, tech debtdocs/product-improvements.md— weaknesses, roadmap, business positioning