Type a plain-English request and watch a multi-agent system design itself in real time: task analysis, agent roles, an animated workflow graph, tool selection with rationales, a memory schema, runnable Python scaffolding, and a simulated execution trace β all streamed live over Server-Sent Events.
Runs fully offline with an intelligent mock engine if no LLM key is set, so the demo never breaks. Drop in a provider key (Groq / Gemini / Anthropic / OpenAI) and it switches to real generation. Any LLM failure silently falls back to the mock engine.
- Architecture
- Project layout
- Prerequisites
- Running the backend β Option A: Docker
- Running the backend β Option B: Python venv
- Running the frontend
- One-shot launcher (macOS / Linux)
- Configuring an LLM provider
- Tech stack and libraries
- API reference
- Pipeline stages
- The generated zip β what to do with it
- Troubleshooting
ββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
β Next.js 14 + Tailwind β SSE β FastAPI (uvicorn) β
β React Flow + lucide-react β βββββββΆ β 3-stage pipeline: β
β / hero /build (4-pane UI) β β PLAN β CODEGEN β SIMLOG β
ββββββββββββββββββββββββββββββββββββββ β β³ LLM provider OR mock β
ββββββββββββββββββββββββββββββββ
No database. No auth. No persistent state β generated specs live in an
in-process Python dict keyed by UUID. The frontend reads everything over a
single SSE stream from POST /api/generate.
agent-forge/
βββ backend/
β βββ main.py FastAPI app, 5 endpoints
β βββ orchestrator.py 3-stage async pipeline; provider dispatch
β βββ mock_engine.py Keyword-driven offline generator (demo safety net)
β βββ prompts.py System prompts for PLAN / CODEGEN / SIMLOG
β βββ requirements.txt Pinned Python deps
β βββ Dockerfile python:3.11-slim image
β βββ .env.example Template for provider keys
β βββ .env Your local keys (gitignored)
βββ frontend/
β βββ app/
β β βββ page.tsx Hero / prompt input
β β βββ build/page.tsx 4-pane builder (tabs Β· graph Β· code Β· terminal)
β β βββ layout.tsx Root layout, fonts
β β βββ globals.css Forge palette, grid backdrop, scrollbars
β βββ components/
β β βββ AgentCards.tsx
β β βββ CodePanel.tsx Typewriter-effect code viewer
β β βββ Terminal.tsx Replays simulation log
β β βββ WorkflowGraph.tsx React Flow with BFS-depth layout
β β βββ StageProgress.tsx
β β βββ nodes/CustomNode.tsx
β βββ lib/
β β βββ api.ts SSE reader + export URL helper
β β βββ types.ts Spec / Agent / Tool / WfNode / LogLine
β βββ tailwind.config.ts Forge ember/cyan palette, pop & glow keyframes
β βββ next.config.js Rewrites /api/* β backend
β βββ tsconfig.json
β βββ package.json
βββ run.sh Convenience launcher (backend + frontend)
βββ README.md
| Component | Version |
|---|---|
| Python | 3.10 or newer (image uses 3.11) |
| Node.js | 18 or newer |
| Docker | Optional β only for the containerised backend |
No system services (Postgres, Redis, etc.) are required.
The provided Dockerfile builds a self-contained image. From the repository
root:
cd backend
# 1. Build the image
docker build -t agent-forge-backend .
# 2. Run it
docker run --rm -p 8000:8000 \
--env-file .env \ # optional β only if you have a key
--name agent-forge-backend \
agent-forge-backendThe container exposes port 8000 and starts uvicorn with
--host 0.0.0.0 --proxy-headers. To skip the env file and run purely in mock
mode, drop the --env-file flag.
Pass env vars inline if you prefer:
docker run --rm -p 8000:8000 \
-e GROQ_API_KEY=gsk_... \
-e MODEL=llama-3.3-70b-versatile \
agent-forge-backendSanity check:
curl http://localhost:8000/api/health
# β {"ok":true,"mode":"groq"} (or "demo (offline mock)")If you'd rather not type long docker run lines, drop this into
backend/docker-compose.yml:
services:
backend:
build: .
ports:
- "8000:8000"
env_file:
- .env
restart: unless-stoppedThen docker compose up --build from backend/.
This is the path used during local development. Hot-reloads on file changes.
cd backend
# 1. Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 2. Install dependencies
pip install --upgrade pip
pip install -r requirements.txt
# 3. (Optional) copy the env template and add a key
cp .env.example .env
# edit .env to add ONE of GROQ_API_KEY / GEMINI_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY
# 4. Run with auto-reload
uvicorn main:app --reload --port 8000On startup you should see:
[orchestrator] python=/.../backend/.venv/bin/python
[orchestrator] PROVIDER=groq MODEL=(default)
INFO: Uvicorn running on http://127.0.0.1:8000
If PROVIDER=None, no key was detected β that's fine, the mock engine handles
everything.
Heads-up.
python-dotenvloads.envonce at import time. After editing the file, restart uvicorn (Ctrl+C βuvicorn main:app --reload).--reloadwatches Python files, not.env.
The frontend is independent of how the backend is started, as long as the
backend is reachable on port 8000 (or whatever you set in NEXT_PUBLIC_API).
cd frontend
npm install
npm run devOpen http://localhost:3000.
frontend/next.config.js proxies /api/* to http://localhost:8000 by
default. To point it elsewhere (e.g. a deployed backend), export
NEXT_PUBLIC_API before npm run dev:
NEXT_PUBLIC_API=https://my-backend.example.com npm run devProduction build:
npm run build
npm run start # serves the prebuilt appFor convenience, run.sh starts both processes:
./run.sh
# Backend on :8000 Β· Frontend on :3000 Β· Ctrl+C to stop bothIt creates the venv if missing, installs Python deps, runs npm install, and
launches both servers in the background. Not suitable for production.
The orchestrator auto-detects exactly one provider, with this priority:
GEMINI_API_KEY β GROQ_API_KEY β ANTHROPIC_API_KEY β OPENAI_API_KEY
Set one in backend/.env:
# Pick ONE block:
# --- Groq (free tier, very fast) ---
GROQ_API_KEY=gsk_...
MODEL=llama-3.3-70b-versatile # default; also: openai/gpt-oss-120b, etc.
# --- Google Gemini ---
# GEMINI_API_KEY=AIza...
# MODEL=gemini-2.5-flash
# --- Anthropic Claude ---
# ANTHROPIC_API_KEY=sk-ant-...
# MODEL=claude-3-5-sonnet-latest
# --- OpenAI ---
# OPENAI_API_KEY=sk-...
# MODEL=gpt-4oIf a real provider call fails for any reason (rate limit, 5xx, malformed JSON, missing key), the orchestrator silently falls back to the mock engine for that stage. The pipeline always completes.
| Library | Version | Why it's here |
|---|---|---|
fastapi |
0.115.0 | HTTP framework, SSE streaming, Pydantic request validation |
uvicorn[standard] |
0.30.6 | ASGI server with --reload, websockets, http-tools |
pydantic |
2.9.2 | Request/response models (GenReq) |
python-dotenv |
1.0.1 | Loads backend/.env on import |
anthropic |
0.39.0 | Native Claude SDK (used only if ANTHROPIC_API_KEY is set) |
openai |
1.55.0 | Native OpenAI SDK (used only if OPENAI_API_KEY is set) |
google-genai |
latest | Google's native Gemini SDK (avoids httpx proxies incompat) |
groq |
latest | Native Groq SDK (used only if GROQ_API_KEY is set) |
Each provider's library is imported lazily β only the active provider's package is touched at runtime, so an outage in one SDK can't crash startup.
| Package | Version | Why |
|---|---|---|
next |
14.2.5 | App-router framework, dev rewrites for /api/* |
react |
18.3.1 | UI runtime |
react-dom |
18.3.1 | DOM renderer |
reactflow |
11.11.4 | Workflow graph with custom nodes |
lucide-react |
0.395.0 | Icon set (Bot, GitBranch, Github, β¦) |
typescript |
5.5.3 | Type-safe components |
tailwindcss |
3.4.4 | Utility CSS β see tailwind.config.ts for the forge palette |
postcss |
8.4.39 | CSS processing pipeline |
autoprefixer |
10.4.19 | Vendor prefixes |
@types/react |
18.3.3 | React types |
@types/node |
20.14.9 | Node types |
Fonts (loaded via Google Fonts in layout.tsx):
- Bricolage Grotesque β display headings
- JetBrains Mono β body, code, terminal
All endpoints are unauthenticated, CORS-open (*), and live under /api/.
| Method | Path | Returns | Notes |
|---|---|---|---|
| GET | /api/health |
JSON | {ok, mode} β mode is the active provider or "demo (offline mock)" |
| GET | /api/examples |
JSON | List of 4 starter prompts |
| POST | /api/generate |
SSE | Body: {"prompt": "..."} β streams data: events through the 3 stages |
| GET | /api/spec/{sid} |
JSON | Full spec for a finished run; 404 if id unknown |
| GET | /api/export/{sid}?format= |
zip|json | format=zip (default) β agent.py + spec.json + requirements.txt + README.md. format=json β raw spec |
The orchestrator (backend/orchestrator.py) runs three LLM calls in
sequence, each in a thread (asyncio.to_thread) so the SSE stream stays
responsive:
- PLAN β given the user prompt, return a JSON spec: analysis, 3β6
agents, tools with rationales, workflow graph (nodes/edges including at
least one decision when applicable), and a memory schema. JSON mode is
enabled (
response_format=json_objectfor OpenAI/Groq,response_mime_type=application/jsonfor Gemini). - CODEGEN β given the JSON spec, emit a single
agent.pyfile using a LangGraph-style structure (TypedDictstate, one function per agent,StateGraphwiring, conditional edges for decisions, tool stubs with docstrings +TODOcomments). - SIMLOG β produce 18β30 simulated log entries (
ts,agent,level,msg) showing realistic handoffs via memory keys, exactly one warning + recovery, and a final success summary.
The exact prompts are in backend/prompts.py β those are the product;
iterate on them more than the surrounding code.
The zip contains four files:
agent.py LangGraph-style scaffold (one function per agent)
spec.json Full plan: agents, tools, workflow, memory schema
requirements.txt Just: langgraph
README.md Minimal "pip install β¦ && python agent.py"
To run as-is:
unzip agent_system_<id>.zip -d my_agent && cd my_agent
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python agent.pyThis will walk the StateGraph and log [Agent Name] running for each node,
then exit. Every agent body is a stub with a TODO comment β no actual
tools are implemented. Think of it as create-next-app for multi-agent
systems: the structure is laid out, you fill in the bodies.
To turn the scaffold into a working agent:
pip installthe real SDK for each tool listed inspec.json(e.g.slack-sdk,PyGithub,openai).- Replace the
TODOline in each agent function with the real API calls, writing results intostate[...]using keys fromspec.json'smemory.schema. - Put secrets (
GITHUB_TOKEN,SLACK_BOT_TOKEN, β¦) in a.envand load withpython-dotenv. - For any
decision-type node inspec.json, replace the linearg.add_edge(...)withg.add_conditional_edges(...)returning the next node id. - Swap the dummy
app.invoke({})for an entrypoint that takes real input.
Build page is stuck on "Awaiting architectureβ¦" forever.
Open the network tab and confirm /api/generate is streaming. If the first
event arrives but nothing follows, the LLM provider is likely retrying
internally β check the uvicorn log for [PLAN LLM ERROR] /
[CODEGEN LLM ERROR] lines (Gemini 503s during peak hours are common). Swap
to Groq or gemini-1.5-flash, or comment the key out to use the mock.
/api/health returns mode: "demo (offline mock)" even though I set a key.
Restart uvicorn after editing .env β python-dotenv only reads it at
import.
The code panel cuts off mid-function.
The codegen response hit max_tokens. Bump the value in
backend/orchestrator.py (currently 8000 across providers; Groq allows up to
~32k for llama-3.3-70b-versatile).
docker build fails compiling a wheel.
The image installs build-essential gcc libffi-dev libssl-dev for exactly
this case. If something newer than what's pinned still fails, rebuild with
--no-cache.
Frontend says "Backend unreachable".
The Next.js rewrite in next.config.js proxies /api/* to
http://localhost:8000. Make sure the backend is listening there, or set
NEXT_PUBLIC_API before npm run dev.
{ "stage": "plan", "status": "running", "data": null, "id": "..." } { "stage": "plan", "status": "complete", "data": { /* spec */ }, "id": "..." } { "stage": "codegen","status": "running", "data": null, "id": "..." } { "stage": "codegen","status": "complete", "data": { "code": "..." }, "id": "..." } { "stage": "simlog", "status": "running", "data": null, "id": "..." } { "stage": "simlog", "status": "complete", "data": { "simulation": [...] }, "id": "..." } { "stage": "done", "status": "complete", "data": null, "id": "..." } { "stage": "stored", "id": "..." }