Author: Mohammad Obaidullah Tusher · AI Engineer · Berlin, Germany
Autonomous AI agent that drafts, evaluates, and publishes LinkedIn posts grounded in personal context. LangGraph + pgvector RAG + cross-model eval. Self-hosted on a Linux home server via Cloudflare Tunnel.
🚧 Status: In active development — target ship 2026-05-10 (14-day MVP). Live demo, eval results, and demo video link below will populate as the build progresses.
- 🚀 Live demo:
https://linkedin-agent.tusher16.com(deploys Day 10) - 🎥 60s walkthrough: (records Day 13)
- 📊 Public LangSmith trace: (share from LangSmith UI after baseline run)
- 📑 Design spec:
docs/superpowers/specs/2026-04-26-linkedin-agent-2week-mvp-design.md
Most "AI LinkedIn post" tools generate generic motivational filler that sounds identical across users. They don't know your thesis, your stack, or your opinions. They also waste tokens — you reject 3 generic drafts before getting one usable post.
This agent is engineered around four explicit decisions:
| Problem | Decision |
|---|---|
| Generic voice — every AI post sounds the same | RAG retrieval over a personal context corpus (pgvector). Every LLM call grounds in author-specific chunks. |
| Token waste — full draft costs ~1000 tokens; rejected drafts compound | Outline-first generation: ~200-token outline → human approves → only then is the full draft generated. ~80% cost reduction on rejected ideas. |
| Self-judge bias — same model judging itself rewards its own style | Cross-model eval: Claude Sonnet 4.6 scores Gemini outputs on a 15-topic dataset. 4 criteria: tone match, technical density, hook strength, AI-cliché detection. |
| No quality gate — bad posts ship without a fight | LangGraph re-draft loop: scores < 7 trigger up to 2 automated re-drafts before the post is even shown to me. |
┌─────────────────────────────────────┐
│ Streamlit Dashboard │
│ (auth · new post · approve · view) │
└────────────────┬────────────────────┘
│ HTTPS via Cloudflare Tunnel
▼
┌─────────────────────────────────────┐
│ FastAPI backend │
│ /draft /approve /publish /list │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ LangGraph StateGraph (typed state) │
│ │
│ guardrails → plan → human-approval │
│ → draft → review → (loop ≤ 2) │
│ → publish │
└────┬───────────────┬────────────────┘
│ │
┌───────▼────┐ ┌─────▼──────────┐
│ pgvector │ │ PostBoost │
│ (RAG │ │ API (real) │
│ context) │ │ │
└────────────┘ └─────────────────┘
│
┌───────▼────────────┐
│ PostgreSQL │
│ users · posts · │
│ context_chunks │
└────────────────────┘
Cross-cutting: LangSmith traces · Pydantic schemas everywhere
Cost cap + iteration cap enforced in state
Prompt-injection guardrails before LLM calls
Six bounded units, each independently testable. See the design spec for the full breakdown.
The agent is benchmarked on 15 hand-curated LinkedIn topics drawn from the author's domain (AI engineering, agent infrastructure, hands-on tooling choices). Drafts are produced by the production graph (Gemini 2.5 Flash); each draft is then scored by Claude Sonnet 4.6 on four criteria:
| Criterion | What it measures |
|---|---|
tone_match |
Voice fit against the author's writing samples |
technical_density |
Substance per sentence; concrete > vague |
hook_strength |
First 1–2 lines compel a click |
ai_cliche_avoidance |
Penalises boilerplate ("game-changer", "leverage", em-dash spam) |
Cross-vendor judging (Gemini drafts, Anthropic judges) reduces same-family bias. Scores are 1–10; a post must average ≥ 7 to pass.
First run, before prompt iteration. Deliberately kept as the starting point of the trajectory.
| Criterion | Avg score |
|---|---|
tone_match |
4.1 |
technical_density |
2.9 |
hook_strength |
3.6 |
ai_cliche_avoidance |
3.9 |
| Overall | 3.6 |
- Pass rate: 0% (gate is avg ≥ 7). 3 of 15 topics errored on
OutlineOutputtruncation — the drafter hit a 600-token output cap mid-JSON. Fixed in130aaf1(cap raised to 1500); that fix lands after this baseline.
Drafter prompts rewritten to attack each criterion by the judge's own definition (first-person + specific lived detail, a concrete number/tool per bullet, banned hedging openers, explicit cliché ban list).
| Criterion | 2026-05-16 | 2026-05-18 |
|---|---|---|
tone_match |
4.1 | 8.7 |
technical_density |
2.9 | 9.0 |
hook_strength |
3.6 | 8.0 |
ai_cliche_avoidance |
3.9 | 9.2 |
| Overall | 3.6 | 8.7 |
- Pass rate: 87% (13/15). Quality gate (avg ≥ 7) passed.
- The 2 non-passes scored 9/10 from the reviewer but hit a
ReviewOutputparse edge case (missingfeedbackfield) — a schema-strictness issue, not draft quality. Tracked as a follow-up. - LangSmith dataset: linkedin-agent-eval-146827fe
- Public trace example: (manual — share a representative trace from the
LangSmith
linkedin-agent-evalproject UI; cannot be scripted)
The smoke test (tests/eval/test_eval_smoke.py) validates the full pipeline
on 3 topics with stub LLMs — no network, runs in <1 s.
Honest note: the first prompt rewrite regressed to 0.0 (token truncation + over-strict refusal) before the fix above. Both baselines are kept — the trajectory, including the dip, is the story.
| Layer | Tool | Why |
|---|---|---|
| Agent framework | LangChain 1.x + LangGraph 1.x | Stateful loops, conditional edges, human-in-the-loop interrupt |
| Observability | LangSmith | Traces every token, latency, cost; eval datasets |
| Drafter LLM | Gemini 2.5 Flash | Fast, cost-efficient |
| Judge LLM | Claude Sonnet 4.6 (Anthropic) | Cross-model judging (different vendor from drafter) |
| Embeddings | text-embedding-3-small |
RAG corpus indexing |
| Vector store | pgvector (in same Postgres) | Single-DB simplicity vs Qdrant — see ADR-001 |
| Schemas | Pydantic 2.x | Typed I/O at every boundary |
| Backend | FastAPI | REST endpoints, JWT auth |
| Dashboard | Streamlit | Auth · new post · pending approvals · history |
| Database | PostgreSQL + pgvector | Drafts, history, vector search |
| Publish | PostBoost | Real LinkedIn publishing via OAuth |
| Reverse proxy | Caddy | Auto-HTTPS within home network |
| Public access | Cloudflare Tunnel | HTTPS without port-forwarding or exposing home IP |
| Monitoring | Uptime Kuma | Self-hosted status page + alerts |
| Backups | pg_dump → Backblaze B2 (encrypted, nightly) |
Off-site disaster recovery |
| CI/CD | GitHub Actions | ruff · mypy · pytest · docker build · GHCR · SSH deploy |
| Test recording | pytest-recording (VCR) |
LLM tests in CI cost $0 |
linkedin-agent/
├── src/linkedin_agent/
│ ├── schemas/ # Pydantic models — single source of truth for I/O
│ ├── tools/ # @tool functions (plan, draft, review, retrieve, publish)
│ ├── graph/ # LangGraph StateGraph + node functions
│ ├── api/ # FastAPI app + auth middleware
│ ├── db/ # SQLAlchemy 2.x models + Alembic + repository
│ └── dashboard/ # Streamlit app
├── tests/
│ ├── unit/ # Schema, tool, chunker tests (VCR cassettes)
│ ├── integration/ # Graph flow, API, repository, RAG (pytest-docker pgvector)
│ ├── e2e/ # Streamlit AppTest dashboard flows
│ ├── security/ # Adversarial input suite (10 attacks)
│ └── eval/ # LangSmith eval smoke tests
├── scripts/
│ ├── run_eval.py # Manual full 15-topic eval runner
│ └── smoke_publish.py # Manual real PostBoost publish smoke test
├── docs/
│ ├── superpowers/specs/ # Design specs
│ ├── adr/ # Architecture decision records
│ └── v2-roadmap.md # Parked features (Telegram, image gen, etc.)
├── archive/
│ ├── 01_base_agent.py # Tutorial-stage code, kept for history
│ └── 02_agent_tools.py # Tutorial-stage code, kept for history
├── docker-compose.yml # api · dashboard · postgres-pgvector · cloudflared · uptime-kuma · caddy
├── Dockerfile # Multi-stage build
├── pyproject.toml # uv-managed deps
├── .env.template # Placeholders only — never real keys
└── README.md
See IMPLEMENTATION_PLAN.md for the day-by-day plan and
TASK.md for the checklist. High-level shape:
- Days 1-5: Schemas, LangGraph, Postgres + pgvector, RAG, eval pipeline.
- Days 6-11: FastAPI, Streamlit, guardrails + adversarial suite, real PostBoost publish, deploy, CI.
- Days 12-14: Polish, ADRs, demo video, recursive demo (the agent writes its own announcement post).
v2 — Parked features (post-MVP, see docs/v2-roadmap.md)
- Telegram bot interface
- Parallel image generation (Gemini Imagen ‖ GPT-image-1) for post hero images
- Playwright/MCP browser-side publish verification
- Multi-provider LLM factory once a second provider is genuinely needed
- Auto-connection-request sub-agent — violates LinkedIn ToS, hard cut.
Local development
# Clone
git clone https://github.com/tusher16/linkedin-agent
cd linkedin-agent
# Install with uv
uv venv --python 3.12
uv sync
# Configure secrets (NEVER commit real keys)
cp .env.template .env # edit with your own keys
# Bring up local Postgres + pgvector
docker compose up -d postgres-pgvector
# Run migrations
uv run alembic upgrade head
# Index personal context into pgvector
uv run python scripts/index_context.py
# Run the FastAPI backend
uv run uvicorn linkedin_agent.api.main:app --reload
# Run the Streamlit dashboard (separate terminal)
uv run streamlit run src/linkedin_agent/dashboard/app.pyRun the test suite
# Unit + integration tests
uv run pytest
# With coverage gate
uv run pytest --cov=src --cov-fail-under=75
# Re-record VCR cassettes (only when prompts change)
uv run pytest --record-mode=rewrite
# Full quality eval (manual, hits real LLMs)
uv run python scripts/run_eval.pyThree short ADRs explain the load-bearing decisions:
- ADR-001 — pgvector over Qdrant
- ADR-002 — Cross-model judge for eval
- ADR-003 — Outline-first generation with human-in-the-loop
.envis in.gitignore. The committed.env.templatecontains placeholders only.- Cloudflare Tunnel exposes the service to the public internet without port-forwarding or revealing the home IP.
- Prompt-injection guardrails run before any LLM call.
- An adversarial test suite of 10 attacks runs in CI and blocks merge on any failure.
Mohammad Obaidullah Tusher — AI Engineer based in Berlin. M.Sc. thesis: semantic segmentation of historical legal documents using LayoutLMv3.
MIT.