A LangGraph-based intelligent agent that generates LinkedIn posts using two distinct execution models: a Human-in-the-Loop (HITL) workflow that pauses for explicit approval, and an autonomous loop that relies on an LLM-driven reviewer to iterate independently. By sharing the exact same writer chain, this project isolates the review mechanism to clearly demonstrate how human gating changes application architecture, state management, and deployment constraints.
Live Frontend: https://linkedin-post-agent-two.vercel.app/
(Note: The backend may spin down after inactivity on the Render free tier, meaning the first request could take up to 30-60 seconds to wake up.)
This project demonstrates two distinct generation flows:
- Human-in-the-Loop (HITL): Generates a draft, suspends execution, and waits for a human to approve or provide feedback.
- Autonomous Generation: Generates a draft, self-evaluates using a separate LLM reviewer node, and iteratively refines the post until it meets quality standards (up to a configured attempt limit).
Building reliable LLM applications often comes down to the tradeoff between autonomy and control. This project tackles the engineering problem of generating LinkedIn content by presenting two identical pipelines that diverge only at the review step.
Comparing human approval with autonomous execution reveals how "agentic" workflows behave differently under supervision. More importantly, it demonstrates how adding a human to the loop drastically changes the backend requirements: an autonomous process runs start-to-finish in a single HTTP request, whereas a human-gated process requires the graph state to be suspended, persisted, and accurately resumed later.
- Human-in-the-Loop Workflow: Pauses LangGraph execution using
interrupt(), awaiting explicit human approval or revision instructions before proceeding. - Autonomous Workflow: Utilizes a structured-output LLM reviewer to automatically grade drafts, passing feedback back to the writer for autonomous iteration.
- Shared Core Logic: Both workflows import the exact same LangChain generation sequence, ensuring accurate side-by-side comparison.
- Web Search Augmentation: Integrates the Tavily search tool (optional) to pull in current data when generating posts.
- State Management: Utilizes LangGraph's
MemorySaverto checkpoint and resume in-process graph state. - React/Vite Frontend: A dedicated client for testing both the interactive HITL flow and the polling-based autonomous background job.
This project isolates the review mechanism to show how the architecture changes when a human is involved.
Human-in-the-Loop (HITL)
User → Writer Node → Extracted Draft → Graph Suspends (interrupt) → Human Review (approve/reject) → Graph Resumes → Final Output
Because the human review step takes time, the graph must pause. This requires a checkpointer (MemorySaver here) and a thread identity so the process can be put down and picked back up later. This single requirement dictates that subsequent requests must land on the same instance that holds the state in memory.
Autonomous
User → Writer Node → Extracted Draft → LLM Reviewer Node (approve/reject) → Loop back to Writer → Final Output
The autonomous loop requires neither suspension nor a checkpointer. It runs from start to finish in one background execution. The challenge here is ensuring the LLM reviewer's verdict is parsed reliably and that it knows when to stop iterating (e.g., maximum attempts).
flowchart LR
subgraph shared["Shared Writer Chain (app/writer.py)"]
direction LR
P[prepare_attempt] --> W[writer]
W -->|tool_calls| T[tools]
T --> W
W -->|prose| X[extract_draft]
end
X --> R{review}
R -->|rejected| P
R -->|approved / max attempts| E[END]
- Frontend: React + Vite + Tailwind UI
- API Layer: FastAPI exposing HTTP routes for starting and resuming graphs
- Agent Orchestration: LangGraph handling state graphs, conditional edges, and interrupts
- LLM Engine: Mistral AI via LangChain for drafting and reviewing
- Tooling: Tavily Search for real-time web context
| Category | Technology |
|---|---|
| Frontend | React (19), Vite (8), Tailwind CSS (4), TypeScript |
| Backend | Python 3.13, FastAPI, Uvicorn |
| Agent Orchestration | LangGraph, LangChain Core |
| AI/LLM | Mistral AI (langchain-mistralai) |
| Search/Tools | Tavily (langchain-tavily) |
| Deployment | Vercel (Frontend), Render (Backend) |
app/api.py: FastAPI routes handling the HITL and Auto endpoints.config.py: Environment checks, model configurations, and logging setup.state.py: ThePostStateschema shared by both graphs.writer.py: The shared LangGraph chain (prepare, write, search, extract).reviewer.py: The structured-output LLM reviewer for the autonomous graph.graph_hitl.py: The HITL graph utilizing theinterrupt()node andMemorySaver.graph_auto.py: The autonomous graph utilizing the LLM reviewer node.jobs.py: In-memory thread and job trackers.
cli_hitl.py&cli_auto.py: Terminal-based runners for debugging without the API.frontend/: The React + Vite client application.
- The frontend calls
POST /api/hitl/startwith a topic. - The
app_hitlgraph executes the shared writer chain. - Upon extracting the draft, the graph hits the
human_reviewnode, callsinterrupt(), and pauses. - The API returns the draft to the frontend with an
awaiting_reviewstatus. - The human reviews the draft and submits feedback via
POST /api/hitl/resume. - The graph resumes. If approved, it routes to
END. If rejected, it routes back toprepare_attemptwith the feedback appended to the state.
- The frontend calls
POST /api/auto/startwith a topic, receiving an immediate HTTP 202 response and ajob_id. - The
app_autograph begins executing in a background thread. - After the writer extracts a draft, it passes to the
reviewernode, which prompts an LLM to evaluate the post. - The reviewer outputs a structured decision (approve/reject + critique).
- The graph loops automatically until approved or the
MAX_ATTEMPTSlimit is reached. - The frontend polls
GET /api/auto/status/{job_id}to stream the history of rejected drafts and the final output.
- Clone the repository and configure the virtual environment:
uv venv VIRTUAL_ENV=.venv uv pip install -r requirements.txt
- Create a
.envfile in the root directory (see Environment Variables). - Start the FastAPI server:
.venv/bin/python -m uvicorn app.api:app --reload
- Navigate to the frontend directory:
cd frontend - Install dependencies:
npm install
- Start the Vite development server:
(Alternatively, run
npm run build && npm run previewnpm run previewwith background execution during development)
| Variable | Used By | Purpose |
|---|---|---|
MISTRAL_API_KEY |
Backend | Required. Authenticates calls to the Mistral AI API for both writing and reviewing. |
TAVILY_API_KEY |
Backend | Optional. Enables web search augmentation if present. |
LOG_LEVEL |
Backend | Optional. Sets Python logging level (e.g., INFO, DEBUG). |
MAX_ATTEMPTS |
Backend | Optional. Maximum revision rounds before the graph forces an end (default: 3). |
CORS_ORIGINS |
Backend | Optional. Allows specific frontend URLs to access the API. |
VITE_API_BASE |
Frontend | Optional. Defines the base URL the frontend uses to contact the backend (e.g., http://localhost:8000). |
Note: Secrets must never be committed to Git. The backend checks for key presence without logging values.
POST /api/hitl/start: Starts the flow. Accepts{ "topic": "...", "enable_search": false }. Returns the first draft,thread_id, and statusawaiting_review.POST /api/hitl/resume: Submits feedback. Accepts{ "thread_id": "...", "feedback": "approved | critique" }. Returns either another draft (awaiting_review) or the final output (completed).
POST /api/auto/start: Initiates the background loop. Accepts{ "topic": "...", "enable_search": false }. Returns202 Acceptedand ajob_id.GET /api/auto/status/{job_id}: Polls for status. Returns job state (running,completed, orerror), current attempt count, history of failed drafts, and the final draft.
GET /api/health: Returns API key presence and current LLM model configuration.
Frontend: Deployed via Vercel at https://linkedin-post-agent-two.vercel.app/. It uses the VITE_API_BASE environment variable to point to the production backend.
Backend: Deployed via Render. It uses CORS_ORIGINS to allow requests explicitly from the Vercel frontend.
Important Deployment Note: The backend currently runs on Render's Free tier, which spins down after 15 minutes of inactivity.
- The first request after a sleep period may take longer (cold start).
- Crucially: Because the HITL flow relies on
MemorySaver(an in-process Python dictionary), if the backend spins down while a draft is awaiting human review, the session state is lost, resulting in a 404 on the next resume request.
- Why LangGraph: LangGraph provides native support for cyclical workflows (loops) and explicit interruptions. This makes it trivial to represent an iterative writing process that routes back on itself.
- Shared Writer Logic: By sharing the exact same
app/writer.pypipeline, the comparison between HITL and Autonomous is isolated entirely to the review node, preventing prompt drift between the two modes. - In-Memory Checkpointing: The project deliberately uses
MemorySaverfor state. While this means state is wiped on server restarts, it perfectly illustrates the architectural cost of human suspension: a real production deployment would require a persistent checkpointer (like Postgres or SQLite) to safely survive multi-instance scaling and cold starts. - Structured Output parsing: The autonomous reviewer attempts to use Pydantic structured output. If unsupported, it fails closed to an anchored regex check, ensuring format drift results in a safe rejection rather than an unverified approval.
- No Authentication / Rate Limiting: The API is unauthenticated and unprotected by rate limits. Anyone with the URL can trigger LLM generation. This is a deliberate tradeoff for a portfolio demo.
- In-Memory Sessions: As noted, scaling the backend beyond one instance or deploying on a scale-to-zero serverless platform (like Render Free) will result in lost HITL sessions because
MemorySaverstate cannot be shared across processes. - Error Handling Check: The architecture explicitly checks for LLM failure states before suspension. If an API call fails, the graph routes straight to
ENDand returns a 502 rather than asking a human to review a system error.
- Persistent Checkpoint Storage: Replace
MemorySaverwithPostgresSaverorSqliteSaverto ensure HITL sessions survive process restarts and allow horizontal scaling. - Authentication & Rate Limiting: Add basic API key validation or user sessions to protect LLM credits.
- Production-Grade Deployment: Upgrade backend hosting to an always-on tier to prevent cold starts and session eviction.
- Evaluation Framework: Introduce an evaluation metric (e.g., LangSmith) to score generated posts against successful real-world LinkedIn content.



