A LangGraph-based multi-agent system where a supervisor orchestrates four specialist AI agents β Researcher, Writer, Editor, and SEO β to autonomously produce a complete, SEO-ready article from a single topic.
Built with a 4-way LLM provider fallback chain (Gemini β Groq β OpenRouter β Cerebras) so the system keeps running even when a free-tier API gets rate-limited.
You give the system one topic. Five AI roles cooperate to turn it into a finished article:
- Researcher gathers current facts from the web (Tavily search)
- Writer turns the research into a full draft
- Editor tightens the draft and can request exactly one rewrite
- SEO produces a title, meta description, keywords, and slug
- Supervisor sits above all of them β it writes no content, it only decides who works next, based on what's been completed so far
The supervisor never hands off to two agents at once and never lets the loop run forever β a deterministic safety net in code enforces valid transitions even if the LLM's routing decision is wrong.
- π§ Adaptive orchestration β not a fixed pipeline; the supervisor routes based on live state
- π Bounded revision loop β editor gets exactly one rewrite chance, enforced in code (not just prompted)
- π 4-provider automatic fallback β Gemini β Groq β OpenRouter β Cerebras; a rate-limit or 404 on one provider silently switches to the next, mid-run
- π₯οΈ Two interfaces β a CLI for scripting, a Streamlit UI for interactive testing with a live handoff log
- β 20 automated tests, all running without any API key (pure logic, no live LLM calls)
- π Structured output everywhere β supervisor routing, editor verdicts, and SEO metadata all use Pydantic schemas, never free-text parsing
- π Defensive design β every LLM decision is double-checked by deterministic code rules
Five roles, one shared state. The supervisor sits in the center β every specialist reports back to it after finishing its job.
graph TD
START([START]) --> SUP[Supervisor]
SUP --> RES[Researcher]
SUP --> WRI[Writer]
SUP --> EDI[Editor]
SUP --> SEO[SEO]
RES --> SUP
WRI --> SUP
EDI --> SUP
SEO --> SUP
SUP --> END([END])
style SUP fill:#7c3aed,color:#fff
style RES fill:#2563eb,color:#fff
style WRI fill:#2563eb,color:#fff
style EDI fill:#2563eb,color:#fff
style SEO fill:#2563eb,color:#fff
style START fill:#16a34a,color:#fff
style END fill:#16a34a,color:#fff
All five nodes read from and write to the same shared state β a single object passed through the whole run, carrying the topic, research, draft, edited draft, and SEO package.
Read left to right. Execution enters at START, goes to the supervisor, which picks exactly one specialist to run next. Every specialist hands control back to the supervisor. Only the supervisor can route to FINISH.
graph LR
START([START]) --> SUP{Supervisor}
SUP -- researcher --> RES[Researcher + Tavily]
SUP -- writer --> WRI[Writer]
SUP -- editor --> EDI[Editor]
SUP -- seo --> SEO[SEO]
SUP -- FINISH --> END([END])
style SUP fill:#7c3aed,color:#fff
style RES fill:#2563eb,color:#fff
style WRI fill:#2563eb,color:#fff
style EDI fill:#2563eb,color:#fff
style SEO fill:#2563eb,color:#fff
style START fill:#16a34a,color:#fff
style END fill:#16a34a,color:#fff
graph LR
A[Gemini] --> B[Groq]
B --> C[OpenRouter]
C --> D[Cerebras]
style A fill:#2563eb,color:#fff
style B fill:#2563eb,color:#fff
style C fill:#2563eb,color:#fff
style D fill:#2563eb,color:#fff
If one provider hits a rate limit or a deprecated-model error, the next one in the chain is tried automatically β including inside structured-output calls (supervisor routing, editor verdict, SEO package).
graph TD
A[Topic] --> B[Researcher: gathers facts]
B --> C[Writer: drafts article]
C --> D[Editor: reviews draft]
D -- APPROVED --> F[SEO: builds metadata]
D -- REVISION, max once --> C
F --> G[Done: article + SEO package]
style A fill:#16a34a,color:#fff
style B fill:#2563eb,color:#fff
style C fill:#2563eb,color:#fff
style D fill:#2563eb,color:#fff
style F fill:#2563eb,color:#fff
style G fill:#16a34a,color:#fff
Key detail: when the writer revises a draft, the previous edited_draft and editor_verdict are cleared from state, so the editor is forced to review the new draft rather than being skipped. This was a real infinite-loop bug found during testing (see Known Issues Fixed below).
supervisor-agent-team/
βββ src/
β βββ config.py # 4-way LLM provider fallback (Gemini/Groq/OpenRouter/Cerebras)
β βββ state.py # Shared state schema (TypedDict)
β βββ prompts.py # System prompts for all agents
β βββ supervisor.py # Routing brain + deterministic safety net
β βββ graph.py # LangGraph wiring (nodes + edges)
β βββ main.py # CLI entry point
β βββ agents/
β βββ researcher.py # Tavily web search + synthesis
β βββ writer.py # Draft + revision (clears stale edit state)
β βββ editor.py # Tightens draft, verdict, revision cap
β βββ seo.py # Structured SEO metadata + slug cleaning
βββ tests/
β βββ test_state.py
β βββ test_supervisor_routing.py
β βββ test_editor_revision_cap.py
β βββ test_seo_slug.py
β βββ test_writer_revision_reset.py # regression test for the infinite-loop bug
β βββ test_no_infinite_loop.py # full state-machine simulation, worst case
βββ streamlit_app.py # UI - provider switch, live log, tabs, download
βββ requirements.txt
βββ .env.example
βββ README.md
# 1. Create a virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure API keys
cp .env.example .envThen open .env and fill in your keys:
| Key | Where to get it | Required? |
|---|---|---|
GOOGLE_API_KEY |
https://aistudio.google.com/apikey (free) | Yes (primary provider) |
GROQ_API_KEY |
https://console.groq.com/keys (free) | Recommended (fallback) |
OPENROUTER_API_KEY |
https://openrouter.ai/keys (free) | Recommended (fallback) |
CEREBRAS_API_KEY |
https://cloud.cerebras.ai (free) | Recommended (fallback) |
TAVILY_API_KEY |
https://app.tavily.com (free) | Yes (researcher agent) |
You only strictly need one LLM provider key to run the system, but having 2+ configured means the fallback chain can actually do its job.
PYTHONPATH=. python -m pytest tests/ -vExpected: 20 passed.
PYTHONPATH=. python -m src.main "How RAG improves LLM accuracy"streamlit run streamlit_app.pyOpens at http://localhost:8501. From there:
- Check the sidebar for API key status (checkmark / cross per provider)
- Pick a provider (or leave on Gemini β fallback handles the rest)
- Enter a topic β Generate Article
- Watch the live handoff log as each agent works
- Review results in three tabs: Article, SEO Package, Full Log
- Download the finished piece as Markdown
If a provider hits a rate limit mid-run, the fallback chain switches automatically β you'll see [fallback] Switching to ... in the log.
A real run's output is saved under output/. Example: output/ai-agents-2026-...md β a full researched, edited, SEO-tagged article with inline citations, produced end-to-end by the agent team with no manual editing.
All tests run without any API key β they test pure logic (state transitions, revision caps, slug cleaning) rather than calling a live LLM. This is deliberate: if tests depended on live APIs, a rate limit would make the test suite itself unreliable β exactly the problem this project's fallback chain exists to solve.
| Test file | What it checks | Why it matters |
|---|---|---|
test_state.py |
initial_state() shape is correct; state isn't accidentally shared across runs |
Foundation β everything else depends on state being correct |
test_supervisor_routing.py |
deterministic_route() picks the right agent for every state combination |
The most critical logic β wrong routing means an infinite loop or a crash |
test_editor_revision_cap.py |
Editor can only request one revision; a second REVISION verdict is force-approved | Prevents the writer-editor loop from running forever |
test_seo_slug.py |
Messy LLM slug output is cleaned into a safe, URL-friendly string | Used directly as a filename downstream β edge cases matter |
test_writer_revision_reset.py |
Writer clears stale edited_draft/editor_verdict after a revision |
Direct regression test for the infinite-loop bug found in production |
test_no_infinite_loop.py |
Full state-machine simulation β even if the editor always says REVISION, the run terminates in a bounded number of steps | Covers the worst-case scenario end-to-end |
PYTHONPATH=. python -m pytest tests/ -vDocumented here deliberately β this is the debugging trail, and it's useful context for anyone reading the code (or asking about it in an interview).
- Infinite writer-supervisor loop. After the writer revised a draft, the old
edited_draftandeditor_verdictstayed in state. The supervisor'sif not edited_draft: route to editorcheck never re-triggered, so the editor stopped being called and the graph looped forever. Fix: the writer now clears those fields whenever it produces a revision. - Structured-output calls bypassed the fallback chain.
with_structured_output()returned a raw LangChain object, so the retry/fallback logic in.invoke()never applied to it β and the supervisor, editor, and SEO agents all use structured output. Fix:with_structured_output()now returns a wrapper that goes through the same fallback chain. - UnicodeEncodeError on Windows. Saving articles with special characters (smart quotes, non-breaking hyphens) crashed on Windows' default
cp1252encoding. Fix: file writes now explicitly specifyencoding="utf-8". - Model deprecation (404 errors). Both Groq (
llama-3.3-70b-versatile) and Cerebras (llama-3.3-70b) deprecated their free-tier Llama models. Fix: defaults updated to currently-supported free-tier models (openai/gpt-oss-120bon both), and model-not-found errors now also trigger the fallback chain, not just rate limits. - Runtime-cached provider selection. The active provider was read once at import time, so switching providers from the Streamlit sidebar had no effect until restart. Fix: the provider is now read fresh from the environment on every LLM call.
- Retry with exponential backoff respecting the
Retry-Afterheader before falling back to the next provider - Streaming token-by-token output in the Streamlit UI instead of waiting for each full agent response
- Persist run history (topic, provider used, duration) for comparison across runs
- Swap the content pipeline for a code-review pipeline (same supervisor pattern, different specialists) as a second demo of the architecture's transferability
- Add a "confidence" field to the editor's verdict so borderline REVISION calls can be surfaced to a human instead of auto-approved
- Dockerfile for one-command setup
- Deterministic routing overrides the LLM. The supervisor's LLM call is used for its reasoning/logging, but the actual next-node decision comes from
deterministic_route()β pure code, no model dependency. A confused or misbehaving LLM cannot send the graph into an invalid state. - Hard revision cap. Regardless of what the editor's LLM says,
revision_countis capped in code β a second REVISION verdict is force-converted to APPROVED. recursion_limit=25is set on every graph invocation as a last-resort circuit breaker, in case a future bug reintroduces a loop.- No silent failures. If all four LLM providers fail,
LLMWithFallbackraises aRuntimeErrorwith the full chain listed β errors are never swallowed. .envis gitignored. API keys are never committed;.env.exampleships with placeholders only.
| Error | Cause | Fix |
|---|---|---|
ModuleNotFoundError: No module named 'src' |
Missing PYTHONPATH |
Prefix commands with PYTHONPATH=. (or $env:PYTHONPATH="." on PowerShell) |
429 / rate limit |
Free-tier quota hit on the active provider | Should auto-fallback; if all providers are exhausted, wait a minute or switch manually in the sidebar |
RESOURCE_EXHAUSTED with limit: 0 (Gemini) |
Your API key/project has no free-tier quota granted at all (not a temporary rate limit) | Check project eligibility in Google AI Studio; try a fresh project/key |
model_not_found / 404 |
Provider deprecated the configured model | Update GROQ_MODEL / CEREBRAS_MODEL / OPENROUTER_MODEL in .env; check the provider's current model list |
ValidationError on structured output |
A weaker model failed to follow the Pydantic schema | Try a stronger model for that provider |
| Tavily search returns nothing | Missing/invalid TAVILY_API_KEY |
Verify the key in .env |
UnicodeEncodeError on save (Windows) |
Already fixed β file writes use encoding="utf-8" explicitly |
Pull the latest main.py / streamlit_app.py |
| Streamlit sidebar provider switch has no effect | Old cached config.py (pre-fix) reads env var once at import |
Update to the current config.py β it reads the provider fresh on every call |
RuntimeError: Event loop is closed after clicking Stop |
Symptom of a long-running/looping script, not the root cause | Fix the underlying loop (see Known Issues #1); this is a Streamlit internal cleanup artifact |
"Multi-agent orchestration" is one of the most commonly discussed AI engineering patterns right now. This project builds one from scratch with a working supervisor, a genuine adaptive routing decision, a bounded revision loop, and a production-grade fallback layer across four LLM providers β not just a single-provider happy-path demo.
The supervisor pattern shown here is directly transferable: swap the four content specialists for a code-review team, a customer-support triage system, or a research pipeline, and the orchestration logic barely changes.