Repository files navigation

✍️ AI Blog Generator

A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline — not a simple LLM wrapper.

PythonFastAPILangGraphGroqStreamlitRenderLangSmith

🔗 Live Demo → | 📖 API Docs →


📌 What This Project Demonstrates

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

🏗️ System Architecture

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 │
└─────────────────────────────────────────────────────────────────┘

The Quality Check Loop — Why This Matters

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.


🚀 Graph Flows

Topic Graph (English)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► END
│
└──(revise)──► content_generation (with feedback)

Language Graph (with Translation)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► route
│ │
└──(revise)──► content hindi / marathi / french → END

🛠️ Tech Stack

LayerTechnologyPurpose
LLMGroq LLaMA 3.1 8B InstantUltra-fast inference for all generation nodes
Agentic FrameworkLangGraph (StateGraph)Multi-node graph with conditional edges and cycles
State ManagementTypedDict + PydanticTyped state flowing through every graph node
APIFastAPI + UvicornREST endpoints with streaming support
ObservabilityLangSmithFull trace of every node, prompt, and LLM call
UIStreamlitLive demo with real-time pipeline progress
Backend HostingRender (Docker)Containerised FastAPI deployment
Frontend HostingStreamlit Community CloudPublic UI deployment

📂 Project Structure

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

⚙️ How It Works

1. Title Creation

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.

2. Outline Generation

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.

3. Content Generation

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.

4. Quality Check (Agentic Loop)

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.

5. Translation (Optional)

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.


🏃 Running Locally

Prerequisites

Setup

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.txt

Environment Variables

cp .env.example .env
# Edit .env and add your keys
GROQ_API_KEY=your_groq_api_key_hereLANGCHAIN_API_KEY=your_langsmith_api_key_here

Run the API

uvicorn 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/docs

Run the Streamlit UI

streamlit run streamlit_app.py
# UI: http://localhost:8501

Run with Docker

docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generator

🔌 API Reference

GET /

Health check.

Response:

{ "status": "ok", "message": "AI Blog Generator API is running." }

POST /blogs

Generate a complete blog post synchronously.

Request:

{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}
FieldTypeRequiredOptions
topicstringMin 3 characters
tonestringprofessional, casual, academic, humorous (default: professional)
languagestringhindi, 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
}

POST /blogs/stream

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}

🎨 Tone Examples

Same topic, four different tones — the difference is striking:

ToneExample 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"

🧠 Key Engineering Decisions

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.


☁️ Deployment

Required Environment Variables

VariableDescription
GROQ_API_KEYGroq API key for LLaMA inference
LANGCHAIN_API_KEYLangSmith API key for tracing

Architecture

  • FastAPI backend → Deployed on Render (Docker, free tier)
  • Streamlit frontend → Deployed on Streamlit Community Cloud
  • Backend URLhttps://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.


⚠️ Disclaimer

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.


📄 License

MIT License — see LICENSE for details.

About

This is an AI-powered Python application that automatically generates high-quality blog posts in seconds. Built with FastAPI, LangGraph, and the Groq AI API, it offers customizable templates, adjustable tone, and multi-language support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

✍️ AI Blog Generator

A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline — not a simple LLM wrapper.

PythonFastAPILangGraphGroqStreamlitRenderLangSmith

🔗 Live Demo → | 📖 API Docs →


📌 What This Project Demonstrates

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

🏗️ System Architecture

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 │
└─────────────────────────────────────────────────────────────────┘

The Quality Check Loop — Why This Matters

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.


🚀 Graph Flows

Topic Graph (English)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► END
│
└──(revise)──► content_generation (with feedback)

Language Graph (with Translation)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► route
│ │
└──(revise)──► content hindi / marathi / french → END

🛠️ Tech Stack

LayerTechnologyPurpose
LLMGroq LLaMA 3.1 8B InstantUltra-fast inference for all generation nodes
Agentic FrameworkLangGraph (StateGraph)Multi-node graph with conditional edges and cycles
State ManagementTypedDict + PydanticTyped state flowing through every graph node
APIFastAPI + UvicornREST endpoints with streaming support
ObservabilityLangSmithFull trace of every node, prompt, and LLM call
UIStreamlitLive demo with real-time pipeline progress
Backend HostingRender (Docker)Containerised FastAPI deployment
Frontend HostingStreamlit Community CloudPublic UI deployment

📂 Project Structure

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

⚙️ How It Works

1. Title Creation

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.

2. Outline Generation

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.

3. Content Generation

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.

4. Quality Check (Agentic Loop)

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.

5. Translation (Optional)

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.


🏃 Running Locally

Prerequisites

Setup

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.txt

Environment Variables

cp .env.example .env
# Edit .env and add your keys
GROQ_API_KEY=your_groq_api_key_hereLANGCHAIN_API_KEY=your_langsmith_api_key_here

Run the API

uvicorn 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/docs

Run the Streamlit UI

streamlit run streamlit_app.py
# UI: http://localhost:8501

Run with Docker

docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generator

🔌 API Reference

GET /

Health check.

Response:

{ "status": "ok", "message": "AI Blog Generator API is running." }

POST /blogs

Generate a complete blog post synchronously.

Request:

{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}
FieldTypeRequiredOptions
topicstringMin 3 characters
tonestringprofessional, casual, academic, humorous (default: professional)
languagestringhindi, 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
}

POST /blogs/stream

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}

🎨 Tone Examples

Same topic, four different tones — the difference is striking:

ToneExample 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"

🧠 Key Engineering Decisions

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.


☁️ Deployment

Required Environment Variables

VariableDescription
GROQ_API_KEYGroq API key for LLaMA inference
LANGCHAIN_API_KEYLangSmith API key for tracing

Architecture

  • FastAPI backend → Deployed on Render (Docker, free tier)
  • Streamlit frontend → Deployed on Streamlit Community Cloud
  • Backend URLhttps://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.


⚠️ Disclaimer

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.


📄 License

MIT License — see LICENSE for details.

About

This is an AI-powered Python application that automatically generates high-quality blog posts in seconds. Built with FastAPI, LangGraph, and the Groq AI API, it offers customizable templates, adjustable tone, and multi-language support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

✍️ AI Blog Generator

A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline — not a simple LLM wrapper.

PythonFastAPILangGraphGroqStreamlitRenderLangSmith

🔗 Live Demo → | 📖 API Docs →


📌 What This Project Demonstrates

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

🏗️ System Architecture

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 │
└─────────────────────────────────────────────────────────────────┘

The Quality Check Loop — Why This Matters

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.


🚀 Graph Flows

Topic Graph (English)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► END
│
└──(revise)──► content_generation (with feedback)

Language Graph (with Translation)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► route
│ │
└──(revise)──► content hindi / marathi / french → END

🛠️ Tech Stack

LayerTechnologyPurpose
LLMGroq LLaMA 3.1 8B InstantUltra-fast inference for all generation nodes
Agentic FrameworkLangGraph (StateGraph)Multi-node graph with conditional edges and cycles
State ManagementTypedDict + PydanticTyped state flowing through every graph node
APIFastAPI + UvicornREST endpoints with streaming support
ObservabilityLangSmithFull trace of every node, prompt, and LLM call
UIStreamlitLive demo with real-time pipeline progress
Backend HostingRender (Docker)Containerised FastAPI deployment
Frontend HostingStreamlit Community CloudPublic UI deployment

📂 Project Structure

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

⚙️ How It Works

1. Title Creation

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.

2. Outline Generation

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.

3. Content Generation

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.

4. Quality Check (Agentic Loop)

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.

5. Translation (Optional)

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.


🏃 Running Locally

Prerequisites

Setup

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.txt

Environment Variables

cp .env.example .env
# Edit .env and add your keys
GROQ_API_KEY=your_groq_api_key_hereLANGCHAIN_API_KEY=your_langsmith_api_key_here

Run the API

uvicorn 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/docs

Run the Streamlit UI

streamlit run streamlit_app.py
# UI: http://localhost:8501

Run with Docker

docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generator

🔌 API Reference

GET /

Health check.

Response:

{ "status": "ok", "message": "AI Blog Generator API is running." }

POST /blogs

Generate a complete blog post synchronously.

Request:

{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}
FieldTypeRequiredOptions
topicstringMin 3 characters
tonestringprofessional, casual, academic, humorous (default: professional)
languagestringhindi, 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
}

POST /blogs/stream

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}

🎨 Tone Examples

Same topic, four different tones — the difference is striking:

ToneExample 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"

🧠 Key Engineering Decisions

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.


☁️ Deployment

Required Environment Variables

VariableDescription
GROQ_API_KEYGroq API key for LLaMA inference
LANGCHAIN_API_KEYLangSmith API key for tracing

Architecture

  • FastAPI backend → Deployed on Render (Docker, free tier)
  • Streamlit frontend → Deployed on Streamlit Community Cloud
  • Backend URLhttps://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.


⚠️ Disclaimer

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.


📄 License

MIT License — see LICENSE for details.

About

This is an AI-powered Python application that automatically generates high-quality blog posts in seconds. Built with FastAPI, LangGraph, and the Groq AI API, it offers customizable templates, adjustable tone, and multi-language support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

✍️ AI Blog Generator

A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline — not a simple LLM wrapper.

PythonFastAPILangGraphGroqStreamlitRenderLangSmith

🔗 Live Demo → | 📖 API Docs →


📌 What This Project Demonstrates

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

🏗️ System Architecture

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 │
└─────────────────────────────────────────────────────────────────┘

The Quality Check Loop — Why This Matters

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.


🚀 Graph Flows

Topic Graph (English)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► END
│
└──(revise)──► content_generation (with feedback)

Language Graph (with Translation)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► route
│ │
└──(revise)──► content hindi / marathi / french → END

🛠️ Tech Stack

LayerTechnologyPurpose
LLMGroq LLaMA 3.1 8B InstantUltra-fast inference for all generation nodes
Agentic FrameworkLangGraph (StateGraph)Multi-node graph with conditional edges and cycles
State ManagementTypedDict + PydanticTyped state flowing through every graph node
APIFastAPI + UvicornREST endpoints with streaming support
ObservabilityLangSmithFull trace of every node, prompt, and LLM call
UIStreamlitLive demo with real-time pipeline progress
Backend HostingRender (Docker)Containerised FastAPI deployment
Frontend HostingStreamlit Community CloudPublic UI deployment

📂 Project Structure

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

⚙️ How It Works

1. Title Creation

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.

2. Outline Generation

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.

3. Content Generation

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.

4. Quality Check (Agentic Loop)

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.

5. Translation (Optional)

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.


🏃 Running Locally

Prerequisites

Setup

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.txt

Environment Variables

cp .env.example .env
# Edit .env and add your keys
GROQ_API_KEY=your_groq_api_key_hereLANGCHAIN_API_KEY=your_langsmith_api_key_here

Run the API

uvicorn 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/docs

Run the Streamlit UI

streamlit run streamlit_app.py
# UI: http://localhost:8501

Run with Docker

docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generator

🔌 API Reference

GET /

Health check.

Response:

{ "status": "ok", "message": "AI Blog Generator API is running." }

POST /blogs

Generate a complete blog post synchronously.

Request:

{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}
FieldTypeRequiredOptions
topicstringMin 3 characters
tonestringprofessional, casual, academic, humorous (default: professional)
languagestringhindi, 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
}

POST /blogs/stream

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}

🎨 Tone Examples

Same topic, four different tones — the difference is striking:

ToneExample 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"

🧠 Key Engineering Decisions

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.


☁️ Deployment

Required Environment Variables

VariableDescription
GROQ_API_KEYGroq API key for LLaMA inference
LANGCHAIN_API_KEYLangSmith API key for tracing

Architecture

  • FastAPI backend → Deployed on Render (Docker, free tier)
  • Streamlit frontend → Deployed on Streamlit Community Cloud
  • Backend URLhttps://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.


⚠️ Disclaimer

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.


📄 License

MIT License — see LICENSE for details.

About

This is an AI-powered Python application that automatically generates high-quality blog posts in seconds. Built with FastAPI, LangGraph, and the Groq AI API, it offers customizable templates, adjustable tone, and multi-language support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

✍️ AI Blog Generator

A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline — not a simple LLM wrapper.

PythonFastAPILangGraphGroqStreamlitRenderLangSmith

🔗 Live Demo → | 📖 API Docs →


📌 What This Project Demonstrates

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

🏗️ System Architecture

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 │
└─────────────────────────────────────────────────────────────────┘

The Quality Check Loop — Why This Matters

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.


🚀 Graph Flows

Topic Graph (English)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► END
│
└──(revise)──► content_generation (with feedback)

Language Graph (with Translation)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► route
│ │
└──(revise)──► content hindi / marathi / french → END

🛠️ Tech Stack

LayerTechnologyPurpose
LLMGroq LLaMA 3.1 8B InstantUltra-fast inference for all generation nodes
Agentic FrameworkLangGraph (StateGraph)Multi-node graph with conditional edges and cycles
State ManagementTypedDict + PydanticTyped state flowing through every graph node
APIFastAPI + UvicornREST endpoints with streaming support
ObservabilityLangSmithFull trace of every node, prompt, and LLM call
UIStreamlitLive demo with real-time pipeline progress
Backend HostingRender (Docker)Containerised FastAPI deployment
Frontend HostingStreamlit Community CloudPublic UI deployment

📂 Project Structure

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

⚙️ How It Works

1. Title Creation

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.

2. Outline Generation

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.

3. Content Generation

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.

4. Quality Check (Agentic Loop)

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.

5. Translation (Optional)

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.


🏃 Running Locally

Prerequisites

Setup

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.txt

Environment Variables

cp .env.example .env
# Edit .env and add your keys
GROQ_API_KEY=your_groq_api_key_hereLANGCHAIN_API_KEY=your_langsmith_api_key_here

Run the API

uvicorn 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/docs

Run the Streamlit UI

streamlit run streamlit_app.py
# UI: http://localhost:8501

Run with Docker

docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generator

🔌 API Reference

GET /

Health check.

Response:

{ "status": "ok", "message": "AI Blog Generator API is running." }

POST /blogs

Generate a complete blog post synchronously.

Request:

{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}
FieldTypeRequiredOptions
topicstringMin 3 characters
tonestringprofessional, casual, academic, humorous (default: professional)
languagestringhindi, 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
}

POST /blogs/stream

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}

🎨 Tone Examples

Same topic, four different tones — the difference is striking:

ToneExample 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"

🧠 Key Engineering Decisions

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.


☁️ Deployment

Required Environment Variables

VariableDescription
GROQ_API_KEYGroq API key for LLaMA inference
LANGCHAIN_API_KEYLangSmith API key for tracing

Architecture

  • FastAPI backend → Deployed on Render (Docker, free tier)
  • Streamlit frontend → Deployed on Streamlit Community Cloud
  • Backend URLhttps://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.


⚠️ Disclaimer

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.


📄 License

MIT License — see LICENSE for details.

About

This is an AI-powered Python application that automatically generates high-quality blog posts in seconds. Built with FastAPI, LangGraph, and the Groq AI API, it offers customizable templates, adjustable tone, and multi-language support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

✍️ AI Blog Generator

A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline — not a simple LLM wrapper.

PythonFastAPILangGraphGroqStreamlitRenderLangSmith

🔗 Live Demo → | 📖 API Docs →


📌 What This Project Demonstrates

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

🏗️ System Architecture

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 │
└─────────────────────────────────────────────────────────────────┘

The Quality Check Loop — Why This Matters

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.


🚀 Graph Flows

Topic Graph (English)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► END
│
└──(revise)──► content_generation (with feedback)

Language Graph (with Translation)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► route
│ │
└──(revise)──► content hindi / marathi / french → END

🛠️ Tech Stack

LayerTechnologyPurpose
LLMGroq LLaMA 3.1 8B InstantUltra-fast inference for all generation nodes
Agentic FrameworkLangGraph (StateGraph)Multi-node graph with conditional edges and cycles
State ManagementTypedDict + PydanticTyped state flowing through every graph node
APIFastAPI + UvicornREST endpoints with streaming support
ObservabilityLangSmithFull trace of every node, prompt, and LLM call
UIStreamlitLive demo with real-time pipeline progress
Backend HostingRender (Docker)Containerised FastAPI deployment
Frontend HostingStreamlit Community CloudPublic UI deployment

📂 Project Structure

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

⚙️ How It Works

1. Title Creation

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.

2. Outline Generation

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.

3. Content Generation

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.

4. Quality Check (Agentic Loop)

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.

5. Translation (Optional)

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.


🏃 Running Locally

Prerequisites

Setup

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.txt

Environment Variables

cp .env.example .env
# Edit .env and add your keys
GROQ_API_KEY=your_groq_api_key_hereLANGCHAIN_API_KEY=your_langsmith_api_key_here

Run the API

uvicorn 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/docs

Run the Streamlit UI

streamlit run streamlit_app.py
# UI: http://localhost:8501

Run with Docker

docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generator

🔌 API Reference

GET /

Health check.

Response:

{ "status": "ok", "message": "AI Blog Generator API is running." }

POST /blogs

Generate a complete blog post synchronously.

Request:

{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}
FieldTypeRequiredOptions
topicstringMin 3 characters
tonestringprofessional, casual, academic, humorous (default: professional)
languagestringhindi, 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
}

POST /blogs/stream

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}

🎨 Tone Examples

Same topic, four different tones — the difference is striking:

ToneExample 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"

🧠 Key Engineering Decisions

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.


☁️ Deployment

Required Environment Variables

VariableDescription
GROQ_API_KEYGroq API key for LLaMA inference
LANGCHAIN_API_KEYLangSmith API key for tracing

Architecture

  • FastAPI backend → Deployed on Render (Docker, free tier)
  • Streamlit frontend → Deployed on Streamlit Community Cloud
  • Backend URLhttps://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.


⚠️ Disclaimer

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.


📄 License

MIT License — see LICENSE for details.

About

This is an AI-powered Python application that automatically generates high-quality blog posts in seconds. Built with FastAPI, LangGraph, and the Groq AI API, it offers customizable templates, adjustable tone, and multi-language support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

✍️ AI Blog Generator

A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline — not a simple LLM wrapper.

PythonFastAPILangGraphGroqStreamlitRenderLangSmith

🔗 Live Demo → | 📖 API Docs →


📌 What This Project Demonstrates

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

🏗️ System Architecture

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 │
└─────────────────────────────────────────────────────────────────┘

The Quality Check Loop — Why This Matters

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.


🚀 Graph Flows

Topic Graph (English)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► END
│
└──(revise)──► content_generation (with feedback)

Language Graph (with Translation)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► route
│ │
└──(revise)──► content hindi / marathi / french → END

🛠️ Tech Stack

LayerTechnologyPurpose
LLMGroq LLaMA 3.1 8B InstantUltra-fast inference for all generation nodes
Agentic FrameworkLangGraph (StateGraph)Multi-node graph with conditional edges and cycles
State ManagementTypedDict + PydanticTyped state flowing through every graph node
APIFastAPI + UvicornREST endpoints with streaming support
ObservabilityLangSmithFull trace of every node, prompt, and LLM call
UIStreamlitLive demo with real-time pipeline progress
Backend HostingRender (Docker)Containerised FastAPI deployment
Frontend HostingStreamlit Community CloudPublic UI deployment

📂 Project Structure

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

⚙️ How It Works

1. Title Creation

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.

2. Outline Generation

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.

3. Content Generation

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.

4. Quality Check (Agentic Loop)

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.

5. Translation (Optional)

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.


🏃 Running Locally

Prerequisites

Setup

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.txt

Environment Variables

cp .env.example .env
# Edit .env and add your keys
GROQ_API_KEY=your_groq_api_key_hereLANGCHAIN_API_KEY=your_langsmith_api_key_here

Run the API

uvicorn 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/docs

Run the Streamlit UI

streamlit run streamlit_app.py
# UI: http://localhost:8501

Run with Docker

docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generator

🔌 API Reference

GET /

Health check.

Response:

{ "status": "ok", "message": "AI Blog Generator API is running." }

POST /blogs

Generate a complete blog post synchronously.

Request:

{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}
FieldTypeRequiredOptions
topicstringMin 3 characters
tonestringprofessional, casual, academic, humorous (default: professional)
languagestringhindi, 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
}

POST /blogs/stream

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}

🎨 Tone Examples

Same topic, four different tones — the difference is striking:

ToneExample 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"

🧠 Key Engineering Decisions

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.


☁️ Deployment

Required Environment Variables

VariableDescription
GROQ_API_KEYGroq API key for LLaMA inference
LANGCHAIN_API_KEYLangSmith API key for tracing

Architecture

  • FastAPI backend → Deployed on Render (Docker, free tier)
  • Streamlit frontend → Deployed on Streamlit Community Cloud
  • Backend URLhttps://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.


⚠️ Disclaimer

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.


📄 License

MIT License — see LICENSE for details.

About

This is an AI-powered Python application that automatically generates high-quality blog posts in seconds. Built with FastAPI, LangGraph, and the Groq AI API, it offers customizable templates, adjustable tone, and multi-language support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

✍️ AI Blog Generator

A production-deployed agentic AI system that generates high-quality blog posts through a self-evaluating LangGraph pipeline — not a simple LLM wrapper.

PythonFastAPILangGraphGroqStreamlitRenderLangSmith

🔗 Live Demo → | 📖 API Docs →


📌 What This Project Demonstrates

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

🏗️ System Architecture

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 │
└─────────────────────────────────────────────────────────────────┘

The Quality Check Loop — Why This Matters

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.


🚀 Graph Flows

Topic Graph (English)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► END
│
└──(revise)──► content_generation (with feedback)

Language Graph (with Translation)

START → title_creation → outline_generation → content_generation
→ quality_check ──(approve)──► route
│ │
└──(revise)──► content hindi / marathi / french → END

🛠️ Tech Stack

LayerTechnologyPurpose
LLMGroq LLaMA 3.1 8B InstantUltra-fast inference for all generation nodes
Agentic FrameworkLangGraph (StateGraph)Multi-node graph with conditional edges and cycles
State ManagementTypedDict + PydanticTyped state flowing through every graph node
APIFastAPI + UvicornREST endpoints with streaming support
ObservabilityLangSmithFull trace of every node, prompt, and LLM call
UIStreamlitLive demo with real-time pipeline progress
Backend HostingRender (Docker)Containerised FastAPI deployment
Frontend HostingStreamlit Community CloudPublic UI deployment

📂 Project Structure

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

⚙️ How It Works

1. Title Creation

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.

2. Outline Generation

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.

3. Content Generation

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.

4. Quality Check (Agentic Loop)

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.

5. Translation (Optional)

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.


🏃 Running Locally

Prerequisites

Setup

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.txt

Environment Variables

cp .env.example .env
# Edit .env and add your keys
GROQ_API_KEY=your_groq_api_key_hereLANGCHAIN_API_KEY=your_langsmith_api_key_here

Run the API

uvicorn 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/docs

Run the Streamlit UI

streamlit run streamlit_app.py
# UI: http://localhost:8501

Run with Docker

docker build -t ai-blog-generator .
docker run -p 8000:8000 \
-e GROQ_API_KEY=your_key \
-e LANGCHAIN_API_KEY=your_key \
ai-blog-generator

🔌 API Reference

GET /

Health check.

Response:

{ "status": "ok", "message": "AI Blog Generator API is running." }

POST /blogs

Generate a complete blog post synchronously.

Request:

{
"topic": "The future of renewable energy",
"tone": "casual",
"language": ""
}
FieldTypeRequiredOptions
topicstringMin 3 characters
tonestringprofessional, casual, academic, humorous (default: professional)
languagestringhindi, 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
}

POST /blogs/stream

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}

🎨 Tone Examples

Same topic, four different tones — the difference is striking:

ToneExample 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"

🧠 Key Engineering Decisions

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.


☁️ Deployment

Required Environment Variables

VariableDescription
GROQ_API_KEYGroq API key for LLaMA inference
LANGCHAIN_API_KEYLangSmith API key for tracing

Architecture

  • FastAPI backend → Deployed on Render (Docker, free tier)
  • Streamlit frontend → Deployed on Streamlit Community Cloud
  • Backend URLhttps://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.


⚠️ Disclaimer

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.


📄 License

MIT License — see LICENSE for details.

About

This is an AI-powered Python application that automatically generates high-quality blog posts in seconds. Built with FastAPI, LangGraph, and the Groq AI API, it offers customizable templates, adjustable tone, and multi-language support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages