A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline β not a simple LLM wrapper.
π Live Demo β Β |Β π API Docs β
This project goes beyond calling an LLM and returning a response. It showcases:
- Designing a multi-node agentic graph with LangGraph including conditional edges and revision cycles
- Implementing a self-evaluation loop where the graph scores its own output and regenerates if quality is insufficient
- Building a streaming REST API with FastAPI that pushes real-time node progress to the client
- Separating concerns across a deployed FastAPI backend (Render) and a Streamlit frontend (Streamlit Cloud)
- Tone-aware generation that injects writing style instructions at every stage of the pipeline
User Request (topic + tone + language)
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FastAPI Backend (Render) β
β β
β POST /blogs POST /blogs/stream β
β (full response) (SSE node-by-node events) β
ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LangGraph Agentic Pipeline β
β β
β ββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββ β
β βtitle_creationββββΊβoutline_generationββββΊβcontent_generat.β β
β ββββββββββββββββ ββββββββββββββββββββ βββββββββ¬βββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββ β
β βββββreviseβββββββ quality_check β β
β β ββββββββββ¬ββββββββββ β
β β β approve β
β βΌ βΌ β
β content_generation ββββββββββββββββ β
β (with feedback) β route β β
β ββββββββ¬ββββββββ β
β ββββββββββββββΌββββββββββββ β
β βΌ βΌ βΌ β
β hindi_trans marathi_trans french β
β ββββββββββββββ΄ββββββββββββ β
β β β
β END β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
LangGraph supports cycles in the graph, which standard LLM chains (LCEL) do not. The quality_check node scores the blog 0β10 and provides actionable feedback. If the score is below 7, the graph routes back to content_generation with the feedback injected into the prompt, forcing the LLM to improve on its previous attempt. This loops up to 3 times before accepting the best result β making this an agentic system, not just a pipeline.
START β title_creation β outline_generation β content_generation
β quality_check ββ(approve)βββΊ END
β
βββ(revise)βββΊ content_generation (with feedback)
START β title_creation β outline_generation β content_generation
β quality_check ββ(approve)βββΊ route
β β
βββ(revise)βββΊ content hindi / marathi / french β END
| Layer | Technology | Purpose |
|---|---|---|
| LLM | Groq LLaMA 3.1 8B Instant | Ultra-fast inference for all generation nodes |
| Agentic Framework | LangGraph (StateGraph) | Multi-node graph with conditional edges and cycles |
| State Management | TypedDict + Pydantic | Typed state flowing through every graph node |
| API | FastAPI + Uvicorn | REST endpoints with streaming support |
| Observability | LangSmith | Full trace of every node, prompt, and LLM call |
| UI | Streamlit | Live demo with real-time pipeline progress |
| Backend Hosting | Render (Docker) | Containerised FastAPI deployment |
| Frontend Hosting | Streamlit Community Cloud | Public UI deployment |
AI-Blog-Generator/
βββ src/
β βββ LLMs/
β β βββ groqllm.py # Groq LLM initialisation
β βββ graphs/
β β βββ graph_builder.py # LangGraph StateGraph construction
β βββ nodes/
β β βββ blog_node.py # All graph node functions + routing logic
β βββ state/
β βββ blog_state.py # BlogState TypedDict + Blog Pydantic model + tone instructions
βββ app.py # FastAPI server β /blogs and /blogs/stream endpoints
βββ streamlit_app.py # Streamlit UI with real-time streaming progress
βββ Dockerfile # Container definition for Render deployment
βββ requirements.txt # Python dependencies
βββ langgraph.json # LangGraph Cloud deployment config
βββ .env.example # Environment variable template
The graph starts by generating a creative, SEO-friendly title using the topic and tone instruction. The tone instruction (e.g. "Write in a witty, humorous tone...") is injected at this stage and every subsequent stage.
Before writing content, the graph produces a structured outline with introduction, 4β6 main sections, and conclusion. This forces the LLM to plan before writing, producing significantly more coherent long-form content.
Full blog content is written following the outline. If this is a revision pass, the quality checker's feedback from the previous attempt is injected into the prompt so the model addresses specific weaknesses.
A senior editor persona scores the blog 0β10 across length, structure, engagement, and tone consistency. If the score is below 7 and fewer than 3 revisions have occurred, the graph routes back to content generation. This is the core agentic behaviour.
After approval, the route node reads current_language from state and conditionally routes to the appropriate translation node (Hindi, Marathi, or French), which preserves markdown formatting.
- Python 3.12+
- Groq API key (console.groq.com)
- LangSmith API key (smith.langchain.com) β optional but recommended
git clone https://github.com/Spandan752/AI-Blog-Generator.git
cd AI-Blog-Generator
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtcp .env.example .env
# Edit .env and add your keysGROQ_API_KEY=your_groq_api_key_here
LANGCHAIN_API_KEY=your_langsmith_api_key_hereuvicorn app:app --host 0.0.0.0 --port 8000 --reload
# API: http://127.0.0.1:8000
# Swagger docs: http://127.0.0.1:8000/docsstreamlit run streamlit_app.py
# UI: http://localhost:8501docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generatorHealth check.
Response:
{ "status": "ok", "message": "AI Blog Generator API is running." }Generate a complete blog post synchronously.
Request:
{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}| Field | Type | Required | Options |
|---|---|---|---|
topic |
string | β | Min 3 characters |
tone |
string | β | professional, casual, academic, humorous (default: professional) |
language |
string | β | hindi, marathi, french (default: English) |
Response:
{
"title": "Why Going Green is Actually Pretty Cool",
"outline": "## Introduction\n## ...",
"content": "## Introduction\n\nLet's be honest β renewable energy...",
"language": "",
"tone": "casual",
"quality_score": 8,
"revision_count": 1
}Stream pipeline events as Server-Sent Events. Each line is a JSON object emitted as each graph node completes.
Request: Same as /blogs
Stream output:
{"node": "title_creation", "title": "Why Going Green is Actually Pretty Cool", ...}
{"node": "outline_generation", ...}
{"node": "content_generation", ...}
{"node": "quality_check", "quality_score": 6, "revision_count": 1}
{"node": "content_generation", ...}
{"node": "quality_check", "quality_score": 8, "revision_count": 2}
Same topic, four different tones β the difference is striking:
| Tone | Example Title |
|---|---|
| Professional | "The Strategic Case for Renewable Energy Investment in 2025" |
| Casual | "Why Going Green is Actually Pretty Cool (and Cheaper Than You Think)" |
| Academic | "Renewable Energy Transition: An Analysis of Adoption Barriers and Policy Frameworks" |
| Humorous | "Sun, Wind, and Zero Guilt: A Love Letter to Renewable Energy" |
Why LangGraph over a simple LCEL chain?
LCEL chains are linear β they can't loop. The quality check revision cycle requires the graph to route back to a previous node based on a score, which is only possible with LangGraph's StateGraph and add_conditional_edges. This is the fundamental reason LangGraph exists.
Why stream events rather than waiting for the full response? Blog generation with quality loops takes 15β30 seconds. A blank screen for that duration destroys the UX. The streaming endpoint emits a JSON event after each node completes, so the UI can show live progress β making the wait feel interactive rather than broken.
Why separate outline and content generation into two nodes? A single "write the blog" prompt produces generic, poorly structured output. Separating outline generation forces the LLM to plan the structure first, then write section by section following that plan. The resulting content is measurably more coherent and better structured.
Why inject tone instructions at every node? Tone drift is a common failure mode β the title might be humorous but the content drifts academic. By injecting the full tone instruction string (not just the word "humorous") at title, outline, and content generation stages, the style stays consistent throughout. The quality checker also validates tone consistency as part of its score.
| Variable | Description |
|---|---|
GROQ_API_KEY |
Groq API key for LLaMA inference |
LANGCHAIN_API_KEY |
LangSmith API key for tracing |
- FastAPI backend β Deployed on Render (Docker, free tier)
- Streamlit frontend β Deployed on Streamlit Community Cloud
- Backend URL β
https://ai-blog-generator-api-favc.onrender.com
β οΈ The free Render tier spins down after 15 minutes of inactivity. The first request after idle may take ~30 seconds to wake up.
This tool generates AI-assisted content intended as a starting point. Always review and edit generated blogs before publishing. AI-generated content should be fact-checked before use.
MIT License β see LICENSE for details.