Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LinkedIn AI Agent

CI coverage python ruff license

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.


Why this exists

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.

Architecture

                     ┌─────────────────────────────────────┐
                     │       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.


Eval methodology

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.

Baseline (2026-05-16, git 700ff1b)

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 OutlineOutput truncation — the drafter hit a 600-token output cap mid-JSON. Fixed in 130aaf1 (cap raised to 1500); that fix lands after this baseline.

After prompt iteration (2026-05-18)

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 ReviewOutput parse edge case (missing feedback field) — 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-eval project 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.


Tech stack

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

Project structure

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

Roadmap

v1 — 14-Day MVP (current)

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

Hard cuts (not on any roadmap)

  • Auto-connection-request sub-agent — violates LinkedIn ToS, hard cut.

Quickstart

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.py
Run 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.py

Architecture decisions

Three short ADRs explain the load-bearing decisions:


Security

  • .env is in .gitignore. The committed .env.template contains 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.

Author

Mohammad Obaidullah Tusher — AI Engineer based in Berlin. M.Sc. thesis: semantic segmentation of historical legal documents using LayoutLMv3.

LinkedIn · GitHub

License

MIT.

About

Autonomous LinkedIn content agent — LangGraph + RAG + pgvector + FastAPI + Streamlit. Self-hosted on Ubuntu via Cloudflare Tunnel.

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages