A full-stack web application where a user submits a research question and a multi-agent system autonomously plans, searches, retrieves, summarizes, and synthesizes a structured report - with every agent step streamed live to a unique terminal-meets-dashboard UI.
Demo query:
"What is the future of AI agents in healthcare?"
| Link | |
|---|---|
| Main (this) | portfolio-research-agent |
| Backend | portfolio-research-agent-backend |
| Frontend | portfolio-research-agent-frontend |
This project is designed to demonstrate the following skills simultaneously:
| Domain | Skills Demonstrated |
|---|---|
| Agentic AI | LangGraph StateGraph, multi-agent orchestration, tool use, streaming events |
| Backend | FastAPI, async Python, WebSocket streaming, REST API design |
| Frontend | Next.js 14 App Router, TypeScript, real-time UI, state management |
| LLM integration | OpenAI SDK-compatible (OpenAI + Groq), prompt engineering, streaming |
| Search | Tavily API (free tier), result ranking and deduplication |
| UI/UX | Unique dark command-center design, live agent trace visualization |
| DevOps | Docker Compose, environment config,.env management |
User Query
│
▼
┌──────────────┐ WebSocket / SSE ┌──────────────────────────┐
│ Next.js │◄─────────────────────────►│ FastAPI │
│ Frontend │ │ │
│ │ REST (submit query) │ ┌────────────────────┐ │
│ Command Bar │──────────────────────────►│ │ Agent Orchestrator│ │
│ Agent Trace │ │ │ (LangGraph) │ │
│ Log Stream │◄── real-time event stream─│ └────────┬───────────┘ │
│ Report View │ │ │ │
│ Sources │ │ ┌────────▼───────────┐ │
└──────────────┘ │ │ Planner Agent │ │
│ │ (breaks subtopics)│ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Researcher Agents │ │
│ │ (parallel per │ │
│ │ subtopic) │ │
│ │ ┌──────────────┐ │ │
│ │ │ Tavily Search│ │ │
│ │ └──────────────┘ │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Summarizer Agent │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Synthesizer Agent │ │
│ │ (final report) │ │
│ └────────────────────┘ │
│ │
│ OpenAI / Groq SDK │
└──────────────────────────┘
The entire agent pipeline is a LangGraph StateGraph. Each agent is a typed node; edges and conditional routing define execution order. This makes the pipeline inspectable, serializable, and easy to extend without touching orchestration logic.
- Explicit graph topology - the planner → researchers → summarizer → synthesizer flow is declared as nodes and edges, not implicit function calls; the graph is the architecture
- Native parallel fan-out - LangGraph's
SendAPI dynamically spawns one Researcher node per subtopic; results are merged back into shared state automatically via a reducer - Built-in streaming -
.astream_events()emits granular events for every node entry, node exit, and LLM token; the FastAPI WebSocket handler maps these directly to the frontend event schema with no manual event management - State as single source of truth - a typed
ResearchStatedict flows through the graph carrying query, provider, model, subtopics, sources, summaries, and final report; no hidden side-channels
START → planner → [Send × N subtopics] → researcher (×N, parallel)
↓ (merge)
summarizer
↓
synthesizer → END
Each Researcher node binds a LangChain tool to its LLM and runs a tool-calling loop until sources are collected:
tavily_search- web search via Tavily API
Every node reads provider and model from the shared state and constructs a ChatOpenAI client (from langchain-openai) on the fly. For Groq, the same class is used with base_url pointed at Groq's OpenAI-compatible endpoint - no separate code path for each provider.
LangGraph's .astream_events() output is consumed by the FastAPI WebSocket handler, which translates each LangGraph event kind into the typed frontend event schema (PLAN_CREATED, SEARCH_DONE, SUMMARY_CHUNK, etc.) before broadcasting over the WebSocket connection.
Four LangGraph nodes run in sequence; Researcher nodes fan out in parallel via Send.
- Input: raw user query
- Output: list of 3–5 subtopics with search strategies
- LLM call with structured output (JSON mode)
- Emits
PLAN_CREATEDevent
- Tavily web search: fetches top 5 results per subtopic (free tier)
- Deduplicates results by URL
- Emits
SEARCH_DONE,SOURCES_COLLECTEDevents per subtopic
- Summarizes each subtopic's collected sources into a concise section
- Streams partial tokens to frontend
- Emits
SUMMARY_CHUNK(streaming) andSUMMARY_DONEevents
- Merges all subtopic summaries into a final structured report
- Sections: Executive Summary, Key Findings, Detailed Analysis, Citations
- Streams final report tokens to frontend
- Emits
REPORT_CHUNK(streaming) andREPORT_DONEevents
Uses OpenAI Python SDK - compatible with both providers:
# OpenAI
client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY)
# Groq (OpenAI-compatible endpoint)
client = openai.AsyncOpenAI(
api_key=GROQ_API_KEY,
base_url="https://api.groq.com/openai/v1"
)User selects provider + model from the UI. Backend reads selection from request payload.
Supported models:
gpt-4o-mini/gpt-4o(OpenAI)llama-3.3-70b-versatile/llama-3.1-8b-instant(Groq)
POST /api/research Submit query → returns session_id
GET /api/research/{id} Get full session result
GET /api/sessions List past sessions
DELETE /api/sessions/{id} Delete session
WS /ws/{session_id} Real-time agent event stream
Every WebSocket message is a typed JSON event:
{
"event": "PLAN_CREATED | SEARCH_DONE | SOURCES_COLLECTED | SUMMARY_CHUNK | SUMMARY_DONE | REPORT_CHUNK | REPORT_DONE | ERROR",
"session_id": "uuid",
"timestamp": "ISO-8601",
"agent": "planner | researcher | summarizer | synthesizer",
"subtopic": "optional string",
"data": {}
}- Python 3.11+
- FastAPI + Uvicorn
- LangGraph - agent graph definition, parallel
Send, state management,.astream_events()streaming langchain-openai-ChatOpenAIwrapping OpenAI SDK; Groq viabase_urloverridelangchain-core-@tooldecorator for Tavily tooltavily-pythonclient- Pydantic v2 for request/response schemas
python-dotenv
Not a standard chat UI. Inspired by:
- IDE terminals (VS Code, Warp)
- Mission control dashboards
- Hacker-aesthetic meets editorial
Visual identity:
- Dark base:
#0a0a0f(near-black with blue undertone) - Accent: electric indigo
#6366f1+ cyan#06b6d4 - Monospace font for log streams and agent traces (JetBrains Mono)
- Serif font for the final report (Playfair Display)
- Subtle animated grid background (CSS only, no canvas)
- Glassmorphism cards with
backdrop-blurfor panels - Framer Motion for agent node animations and transitions
┌─────────────────────────────────────────────────────────────────┐
│ HEADER: logo + provider selector (OpenAI / Groq) + model pick │
├─────────────────────────────────────────────────────────────────┤
│ COMMAND BAR: full-width query input (VS Code palette style) │
│ [Enter to Research >>] │
├──────────────┬──────────────────────────┬───────────────────────┤
│ │ │ │
│ AGENT TRACE │ LIVE LOG STREAM │ SOURCES PANEL │
│ (left 20%) │ (center 50%) │ (right 30%) │
│ │ │ │
│ Visual tree │ Timestamped log lines │ Source cards: │
│ of agents │ with color-coded │ - title │
│ and their │ event types │ - domain badge │
│ status: │ │ - relevance score │
│ │ ● PLAN_CREATED │ - snippet │
│ ○ Planner │ ● SEARCH: subtopic 1 │ - [open link] │
│ ├○ Research │ ● SEARCH: subtopic 2 │ │
│ ├○ Research │ ● SUMMARY streaming... │ Expandable per │
│ └○ Synthsz │ ● REPORT streaming... │ subtopic │
│ │ │ │
├──────────────┴──────────────────────────┴───────────────────────┤
│ REPORT PANEL (collapsible, expands below on REPORT_DONE) │
│ Structured markdown report with section headers, citations │
│ [Copy] [Export MD] [New Research] │
├─────────────────────────────────────────────────────────────────┤
│ SESSION HISTORY (collapsible bottom drawer) │
│ Past queries with timestamps - click to reload │
└─────────────────────────────────────────────────────────────────┘
| Component | Description |
|---|---|
CommandBar |
Full-width input with animated placeholder cycling through example queries |
ProviderSelector |
Dropdown to pick OpenAI or Groq + model within each |
AgentTraceTree |
Animated vertical tree, nodes pulse when active, checkmark on done |
LogStream |
Auto-scrolling terminal-style log with color per event type |
SourcesPanel |
Tabbed by subtopic, cards with favicon, domain badge, score bar |
ReportViewer |
Streaming markdown renderer, serif font, section anchors, TOC |
SessionDrawer |
Bottom slide-up list of past sessions from /api/sessions |
StatusBar |
Footer: current agent, token count, latency |
- WebSocket connection opened immediately on query submit
LogStreamappends each event as a formatted lineReportViewerrenders tokens incrementally asREPORT_CHUNKevents arrive- Agent nodes in
AgentTraceTreetransition:idle → active → done → error
- Zustand store:
useResearchStoresession,events[],sources[],reportChunks[],agentStatuses{}
- No Redux, no prop drilling
- Next.js 14 (App Router, TypeScript)
- Tailwind CSS + custom design tokens
- Framer Motion (agent tree animations, panel transitions)
- Zustand (state)
react-markdown+remark-gfm(report rendering)- JetBrains Mono + Playfair Display (Google Fonts)
date-fns(log timestamps)
# Backend (.env)
OPENAI_API_KEY=sk-...
GROQ_API_KEY=gsk_...
TAVILY_API_KEY=tvly-...
DEFAULT_LLM_PROVIDER=openai
DEFAULT_MODEL=gpt-4o-mini
# Frontend (.env.local)
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_WS_URL=ws://localhost:80001.research-agent/
├── backend/
│ ├── main.py # FastAPI app, WebSocket handler, event translator
│ ├── graph.py # LangGraph StateGraph definition and compilation
│ ├── state.py # ResearchState TypedDict
│ ├── agents/
│ │ ├── planner.py # LangGraph node: plan subtopics
│ │ ├── researcher.py # LangGraph node: tool-calling loop (Tavily)
│ │ ├── summarizer.py # LangGraph node: per-subtopic summary
│ │ └── synthesizer.py # LangGraph node: final report
│ ├── tools/
│ │ └── tavily_search.py # @tool: Tavily web search
│ ├── models/
│ │ └── schemas.py # Pydantic request/response models
│ ├── services/
│ │ └── llm.py # ChatOpenAI factory (OpenAI + Groq via base_url)
│ └── requirements.txt
├── frontend/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx # Main research page
│ │ └── api/ # Next.js route handlers (proxy)
│ ├── components/
│ │ ├── CommandBar.tsx
│ │ ├── ProviderSelector.tsx
│ │ ├── AgentTraceTree.tsx
│ │ ├── LogStream.tsx
│ │ ├── SourcesPanel.tsx
│ │ ├── ReportViewer.tsx
│ │ ├── SessionDrawer.tsx
│ │ └── StatusBar.tsx
│ ├── store/
│ │ └── useResearchStore.ts
│ ├── types/
│ │ └── events.ts
│ └── package.json
├── docker-compose.yml # backend + frontend
└── workspace.md
- No authentication - this is a portfolio demo; any user can submit a query and view past sessions. No login, no user isolation.
- Real agentic framework - uses LangGraph
StateGraphwith typed state, conditional fan-out viaSend, and built-in streaming; not a hand-rolled loop or "LangGraph-style" approximation - Multi-agent system - planner → parallel researchers → summarizer → synthesizer; each is a discrete node with a single responsibility
- Real full-stack - async FastAPI backend, React frontend, WebSocket streaming, external APIs
- Provider flexibility -
ChatOpenAIfromlangchain-openaiused as a universal interface; Groq swapped in viabase_urlwith zero extra code - Live observability - LangGraph's
.astream_events()feeds every node transition and LLM token to the UI in real time; nothing hidden - Unique UI - command-center design, not a generic chat template; shows frontend design skill alongside AI skill
- Free-tier viable - Tavily free tier + Groq free tier means it runs at zero cost for demos
