An application for supervisor based multi agent workflows with LangGraph, MCP tool boundaries, human approval, and a React monitoring console.
- Start the backend stack from the repository root:
docker compose up --build. - Start the frontend in a second terminal:
Set-Location frontend; npm install; npm run dev. - Open
http://localhost:5173.
The API runs at http://localhost:8000; interactive documentation is at http://localhost:8000/docs.
For full instructions, see:
- Backend README: Docker, FastAPI, PostgreSQL, Redis, LangGraph, MCP, API calls, approval flow, and tests.
- Frontend README: React setup, environment variables, polling, approval UI, and frontend builds.
- MIT License
This is the simplest full-stack development path. Docker starts PostgreSQL, Redis, and the FastAPI container. The frontend still runs from its own folder.
- Docker Desktop with Compose
- Node.js 18 or newer
- npm
From the repository root:
Copy-Item .env.example .env
docker compose up --buildThe first build installs the Python dependencies and may take a few minutes. The services are then available at:
| Service | URL or address |
|---|---|
| FastAPI | http://localhost:8000 |
| FastAPI docs | http://localhost:8000/docs |
| PostgreSQL | localhost:5432 |
| Redis | localhost:6379 |
The backend waits for the PostgreSQL and Redis Docker health checks before starting. To run it in the background instead:
docker compose up --build -d
docker compose ps
docker compose logs -f backendStop the stack with:
docker compose downAdd -v only when you intentionally want to delete the PostgreSQL and Redis volumes:
docker compose down -vOpen a second terminal:
Set-Location frontend
Copy-Item .env.example .env
npm install
npm run devOpen http://localhost:5173. The frontend defaults to http://localhost:8000/api/v1, so no extra configuration is needed for Option A.
This mode is useful while learning or debugging Python and React code.
From the repository root, start only the infrastructure services:
docker compose up postgres redisLeave this terminal running. The backend should use localhost for these services when it runs outside Docker. The root .env.example is for Compose and uses Docker service names, so create backend/.env with localhost values for local FastAPI development:
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=multiagent
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
REDIS_HOST=localhost
REDIS_PORT=6379Create and activate a virtual environment, then install the backend dependencies:
Set-Location backend
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000If PowerShell blocks activation, run this once in the current user scope or use the Python executable directly:
Set-ExecutionPolicy -Scope CurrentUser RemoteSignedSet-Location frontend
npm install
npm run devThen open http://localhost:5173.
To point the frontend at another backend, create frontend/.env:
VITE_API_BASE_URL=http://localhost:8000/api/v1Restart Vite after changing a VITE_ environment variable.
When using Option B, the root .env is only read by Docker Compose and backend/.env is read by the locally started FastAPI process. Do not confuse the two files.
- Open
http://localhost:5173. - Enter a question in the research brief and select Start research.
- The frontend stores the returned
run_idand polls the run status and event endpoints every 1.2 seconds. - Watch the workflow timeline move through the input guardrail, supervisor, parallel research, analyst, critic, and synthesizer nodes.
- Wait for
awaiting_approval. Saving a report is a medium-risk action, so it does not happen automatically. - Edit the filename if needed, then choose Approve, Edit & approve, or Reject.
- After approval, the backend resumes from the synthesized run state, calls the workspace MCP tool, and exposes the result in the frontend.
The current demo uses deterministic local data. No Gemini key is required to run the default workflow.
Start a run:
$run = Invoke-RestMethod -Method Post `
http://localhost:8000/api/v1/research `
-ContentType 'application/json' `
-Body '{"query":"Compare PostgreSQL and Pinecone for a RAG application"}'
$runPoll status, events, and approval:
$id = $run.run_id
Invoke-RestMethod http://localhost:8000/api/v1/runs/$id
Invoke-RestMethod http://localhost:8000/api/v1/runs/$id/events
Invoke-RestMethod http://localhost:8000/api/v1/runs/$id/approvalApprove the generated filename:
Invoke-RestMethod -Method Post `
http://localhost:8000/api/v1/runs/$id/approval `
-ContentType 'application/json' `
-Body '{"decision":"approve"}'Edit and approve instead:
Invoke-RestMethod -Method Post `
http://localhost:8000/api/v1/runs/$id/approval `
-ContentType 'application/json' `
-Body '{"decision":"edit","arguments":{"filename":"database-comparison.md"}}'Reject the action:
Invoke-RestMethod -Method Post `
http://localhost:8000/api/v1/runs/$id/approval `
-ContentType 'application/json' `
-Body '{"decision":"reject","reason":"Do not save this report."}'Read the completed result:
Invoke-RestMethod http://localhost:8000/api/v1/runs/$id/resultHealth checks:
Invoke-RestMethod http://localhost:8000/api/v1/health
Invoke-RestMethod http://localhost:8000/api/v1/health/dependenciesflowchart TD
Client[React polling console] --> API[FastAPI]
API --> Guard[Input guardrail]
Guard --> Supervisor[Supervisor / planner]
Supervisor -->|LangGraph Send| ResearchA[Research task A]
Supervisor -->|LangGraph Send| ResearchB[Research task B]
ResearchA --> MCP[MCP client boundary]
ResearchB --> MCP
MCP --> Analyst[Analyst]
Analyst --> Critic[Critic]
Critic --> Synth[Synthesizer]
Synth --> Approval{Medium-risk save_report}
Approval -->|approve or edit| Workspace[Workspace MCP]
Approval -->|reject| Rejected[Controlled rejection]
Workspace --> Output[Output guardrail and result]
backend/app/graph.pycontains the explicitStateGraphandSendfan-out example. The supervisor creates two tasks andSendroutes each task to the researcher node concurrently.backend/app/workflow.pycoordinates the run lifecycle and keeps the run state readable.- The current analyst and critic are deterministic teaching implementations. The Gemini provider boundary is in
backend/app/llm.pyand readsGEMINI_API_KEYandGEMINI_MODELfrom the environment when enabled. GET /runs/{run_id}provides current node, progress, status, and tasks.GET /runs/{run_id}/eventsprovides a small pollable activity feed.- The frontend does not calculate workflow progress. It renders the backend's
progress,current_node, task statuses, and events.
The first version keeps MCP deterministic and local so it can be learned and tested without external accounts. The three server modules are:
| Server | Tools | Current behavior |
|---|---|---|
| Search | search, fetch_url |
Returns deterministic local evidence |
| Documents | list_documents, read_document |
Reads Markdown files from backend/data/documents/ |
| Workspace | create_report, save_report |
Writes reports to backend/data/reports/ |
Agents call MCPClient.call(server, tool, arguments) in backend/app/mcp.py; they do not import these server modules directly. The current client is an in-process adapter, not three independently running services. This is intentional for Project 1. The client boundary can later be replaced with MCP stdio or HTTP transport without changing the agent workflow.
To try the document server, place a Markdown file in backend/data/documents/, then call the client from a Python shell or add a document task to the workflow. Approved reports appear in backend/data/reports/. The Docker backend mounts ./backend/data into the container so those files remain visible on the host.
Copy .env.example to .env at the repository root. Important values include:
GEMINI_API_KEY=
GEMINI_MODEL=gemini-2.0-flash
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=multiagent
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
REDIS_HOST=redis
REDIS_PORT=6379
MAX_RESEARCH_RETRIES=2Never commit .env or place secrets in frontend code. VITE_ variables are bundled into browser JavaScript, so a Gemini API key must never be put in frontend/.env.
PostgreSQL models and an async database adapter are present in backend/app/persistence_models.py and backend/app/database.py. The learning workflow currently uses an in-memory run repository so it works immediately in local tests. The current /health/dependencies response reports infrastructure as not configured until real repository, Redis, and LangGraph PostgreSQL checkpointer wiring is enabled.
Likewise, CacheService is currently an in-process replaceable adapter. Its interface is get, set, and delete, and only completed repeatable research results are cached. Approval state and mutable workflow state are not cached.
From backend/:
python -m pytest tests -q
python -m compileall app mcp_serversFrom frontend/:
npm install
npx tsc -b
npx vite buildThe tests use deterministic MCP behavior and do not require a real Gemini API key, PostgreSQL connection, Redis connection, or external network service.
backend/
app/
main.py FastAPI routes
workflow.py asynchronous research and approval lifecycle
graph.py LangGraph StateGraph and Send example
mcp.py MCP client boundary
guardrails.py input and output checks
policy.py action risk classification
persistence_models.py PostgreSQL model definitions
mcp_servers/ deterministic local MCP server implementations
tests/
frontend/
src/api/ centralized API calls
src/types/ API types
src/App.tsx research console and workflow views
src/styles.css responsive visual system
flowchart TD
API[FastAPI] --> G[Input guardrail]
G --> S[Supervisor LangGraph]
S -->|Send fan-out| R1[Research task]
S -->|Send fan-out| R2[Research task]
R1 --> M[MCP client]
R2 --> M
M --> A[Analyst and critic]
A --> Y[Synthesizer]
Y --> H{Save report approval}
H -->|approve/edit| W[Workspace MCP]
H -->|reject| X[Controlled rejection]
backend/app/graph.py is the small executable LangGraph teaching example. The workflow uses it to plan two concurrent research tasks, then calls the MCP client boundary. backend/app/database.py and persistence_models.py define the PostgreSQL persistence boundary for runs, tasks, approvals, and artifacts; the in-memory repository is used by default for deterministic learning and tests. Replace it with the SQL repository and a LangGraph PostgreSQL checkpointer when wiring a deployed environment.
Medium and high risk actions are represented by an Approval and pause at awaiting_approval. POST an approval with approve, edit plus arguments, or reject plus a reason. No report write occurs before approval. Redis caching is limited to completed, repeatable research results; the included cache is an in-process adapter so the API runs without infrastructure.
$run = Invoke-RestMethod -Method Post http://localhost:8000/api/v1/research -ContentType 'application/json' -Body '{"query":"compare solar and wind energy"}'
Invoke-RestMethod http://localhost:8000/api/v1/runs/$($run.run_id)
Invoke-RestMethod -Method Post http://localhost:8000/api/v1/runs/$($run.run_id)/approval -ContentType 'application/json' -Body '{"decision":"approve"}'
Invoke-RestMethod http://localhost:8000/api/v1/runs/$($run.run_id)/resultGemini configuration is reserved for the provider abstraction and comes from GEMINI_API_KEY and GEMINI_MODEL; no key is required for the deterministic local workflow.
