LinkedIn Engagement Manager (LEM) is an automated solution for managing engagement and post interactions on LinkedIn. It builds a 30-day content plan, publishes AI-generated posts (text, carousels, native video, native PDF documents) around peak hours, and runs the day-to-day engagement — feed commenting, replies, direct messages, and follow-ups — on your behalf. All content is AI-generated via a LiteLLM proxy, passes through quality gates (a slop lint and an authenticity score, with content-quality telemetry underneath), and can flow through a preview/approval workflow before publishing. Engagement is shaped by per-user targeting, voice/tone, and daily-cap preferences.
- Buyer-journey content plan: A balanced 30-day plan across awareness / consideration / decision stages — thought-leadership, industry-news commentary, personal-story, and engagement-prompt posts, carousels (educational / case-study / product-demo / insights), native video, native PDF documents, and blog summaries. The plan balances four post types (
PLANNED_POST_TYPESinsrc/cqc_lem/app/run_content_plan.py) and never schedules two posts inside 24 hours. - AI-Generated Content: LiteLLM-proxied models generate text and carousel content with tiered routing (
lem-simple,lem-medium,lem-complex,lem-image); video is rendered by RunwayML (src/cqc_lem/utilities/ai/video_models.py) from a LiteLLM-written prompt and source frame, with Pexels stock as the fallback. - Peak-hour scheduling: Auto-schedules posts around golden/peak hours, with self-healing carousels and media asset backfill.
- Quality gates & approval workflow: Drafts go through a slop lint and an authenticity score, and
src/cqc_lem/utilities/content_quality.pytracks the trend; preview and approve manually or let it auto-approve. NativeLinkedInPostPreviewcomponent and a date-time picker for editing scheduled posts.
- Feed commenting: SDUI-resilient feed interaction with a recency-dominant scoring matrix (recency + relevance + reciprocity + activity), inline compose/submit, and best-effort "Recent" feed sort. Runs shortly before each scheduled post and daily at a golden hour.
- Replies, seed & pin: Replies to comments on your own posts and auto-seeds + pins a first comment on your posts.
- Reciprocity: Tracks who engaged with you and prioritizes commenting back.
- Direct messages: Appreciation DMs (connections / recommendations / collaborations), profile-viewer outreach, and multi-touch follow-up sequences — all templated and voice-aligned.
- Company-page invitations: A daily, per-day-capped drip at each user's staggered slot (
invite_to_company_pagesinsrc/cqc_lem/app/my_celery.py); it replaced an earlier once-a-month blast.
- Targeting: Include/exclude topics, keywords, and authors; minimum reactions and maximum post age; LLM topic-relevance scoring.
- Voice: Tone, comment length (short/medium/long), style, and emoji/hashtag toggles.
- Caps: Per-day comment and DM limits; DM template editor with follow-up steps; Login Location (city/state geocoding).
- Anti-bot infra: Per-user proxy (
users.proxy_url, optionally a residential provider — seedocs/PER_USER_PROXY.md) with an MV3 proxy-auth extension, cookie persistence, and an email-PIN LinkedIn verification flow; 429/auth-wall backoff. - Dockerized Environment: One Docker Compose stack, run locally or on a VPS.
- Modular Design: Content generation providers are swappable via LiteLLM aliases.
- React SPA Dashboard: Mobile- and web-friendly dashboard for monitoring and controlling engagement.
- Observability: PostHog-based LLM usage tracking, Celery task metrics, and API latency monitoring.
- Python 3.12+ with FastAPI for the backend API.
- React 19 + Vite 8 + Tailwind CSS 4 for the single-page frontend (
src/cqc_lem/ui/package.json). - Selenium 4 (
selenium/standalone-chrome) for LinkedIn browser automation. - Celery + Redis for distributed task scheduling and execution.
- MySQL 8 as the relational database.
- LiteLLM (port 4000) as an AI proxy routing to Ollama Cloud, OpenAI, Anthropic, and Perplexity. An
OPENROUTER_API_KEYis passed through to the proxy container, but no alias in.litellm/config.yamluses it. - Docker Compose for local orchestration and for the VPS deploy — the only supported deploy path.
- PostHog for observability (LLM cost, task metrics, API latency).
- Poetry for Python dependency management.
- Docker installed on your system.
- Python 3.12+ for local development (managed via Poetry).
- Poetry for Python package management (
pip install poetry). - Node.js for frontend development (only needed if editing the React UI; CI builds the UI with Node 24, see
.github/workflows/ui-build.yml). - API keys for the LLM providers wired up in
.litellm/config.yaml: Ollama Cloud (OLLAMA_CLOUD_API_KEY) serves the text tiers, and OpenAI (OPENAI_API_KEY) sits in the same text-tier groups (under latency-based routing it competes rather than waits) and is the only provider for the image, vision, embedding, and TTS aliases. Anthropic and Perplexity keys are optional. - (Optional) ngrok for exposing local services publicly:
brew install ngrok/ngrok/ngrok
-
Clone the repository:
git clone https://github.com/christopherqueenconsulting/linkedin_engagement_manager.git cd linkedin_engagement_manager -
Set up the environment:
cp .env.example .env # Fill in required values: MySQL credentials, LinkedIn credentials, # AI API keys, LiteLLM master key, Docker image name, etc.
-
(Optional) Configure ngrok by setting
NGROK_PLAN(tunnels are off by default),NGROK_AUTH_TOKEN,NGROK_CUSTOM_DOMAINorNGROK_FREE_DOMAIN, and theNGROK_*_PREFIXvariables in your.envfile (seerun.shandgenerate-ngrok-config.sh). -
Set up Stripe for subscription billing — see docs/stripe-setup-guide.md for the full walkthrough. At minimum you need:
STRIPE_API_KEY=sk_test_... STRIPE_PRICE_ID_STARTER=price_... STRIPE_PRICE_ID_PROFESSIONAL=price_... STRIPE_PRICE_ID_ENTERPRISE=price_... STRIPE_WEBHOOK_SECRET=whsec_...
The webhook secret comes from registering your ngrok URL as a Stripe webhook endpoint (same pattern as the LinkedIn OAuth redirect URL). Do not use
stripe listeninside Docker — it generates an ephemeral secret that breaks on every restart. -
Build and run the Docker containers:
./run.sh
The script will prompt whether to build and push the Docker image, then start all services and print a table of local (or ngrok) URLs.
-
Access the services at the URLs printed in the console.
The main application image (${DOCKER_IMAGE_NAME}:latest) is shared by the web_app
(FastAPI), celery_worker, the four celery_worker_selenium* workers, celery_beat, and
flower services in docker-compose.yml. The run.sh script handles building and optionally
pushing this image to Docker Hub; the release train (.github/workflows/build-and-push.yml)
pushes to GHCR instead — see docs/DEPLOYMENT.md.
App LLM calls go through the LiteLLM proxy at http://litellm:4000; the exceptions are the direct Perplexity helper in src/cqc_lem/utilities/ai/tools.py (the lem-research fallback), Replicate for avatar images, and RunwayML for video. Model tier
aliases are defined in .litellm/config.yaml:
| Alias | Use case |
|---|---|
lem-simple |
Fast, lightweight — refine / summarize / list tasks |
lem-medium |
Balanced: comments, post refinement |
lem-complex |
Long-form: thought leadership, personal story |
lem-image |
Image generation (gpt-image-2, gpt-image-1 in-group fallback) |
lem-vision |
Render quality gate — looks at a generated image (gpt-4o-mini) |
lem-router |
Auto-routes to a tier by prompt shape (LEMComplexityRouter in .litellm/complexity_router.py) |
The same file also defines lem-research (Perplexity Sonar), lem-tts, lem-embedding, and the
lem-agent-* tiers used by the agent pipeline. Deployments inside a group compete under
latency-based-routing; order is not a priority list.
This project uses pytest, in two lanes: unit (all I/O mocked) and integration (real MySQL + Redis).
There is no browser lane — a change that needs a real browser is graded by the read-only live probe,
scripts/linkedin_live_validation.py. See tests/README.md for why (#1215).
-
Install test dependencies:
poetry install --with test -
Run all tests:
poetry run pytest
-
Run tests with coverage:
poetry run pytest --cov=src/cqc_lem --cov-report=html --cov-report=term
-
Run specific test categories:
# Run only unit tests poetry run pytest tests/unit -v --tb=short # Run only integration tests (requires MySQL + Redis) poetry run pytest tests/integration -v
-
Run specific test file:
poetry run pytest tests/unit/utilities/test_db.py -v
-
Run tests matching a pattern:
poetry run pytest -k "test_database" -v
Tests are organized with the following markers:
unit— Fast tests with all external I/O mockedintegration— Require real MySQL + Redis service containersslow— Tests that take longer to executerequires_openai— Tests requiring OpenAI API accessrequires_database— Tests requiring a database connectionrequires_selenium— Tests requiring browser automation
pyproject.toml also registers requires, asyncio, and compile; --strict-markers is on, so
an unregistered marker fails collection.
Targets are set in codecov.yml:
- Patch: 80% of changed lines, 5% threshold
- Project: 93%, 1% threshold
- Per-component statuses (API, Celery tasks, and so on) target 90% and are informational only
Codecov posts these statuses on every PR, but they are not among the required branch-protection checks listed below.
Branch protection on main requires these status checks before a PR can merge:
Unit Tests (Python 3.12)(unit-tests.yml)Integration Tests(integration-coverage.yml)GitGuardian Scan(gitguardian-scan.yml)UI Build(ui-build.yml)Migration Versions(migration-check.yml)CodeQL PR Quality Gate(codeql-pr-gate.yml)Docstring & Lint Gate(docstring-lint.yml)
CodeQL Security Analysis (codeql-analysis.yml) also runs on push, PR, and a schedule, but is
not a required check.
The test workflows run on:
- Every push to
main - All pull requests targeting
main, and merge-queue runs
There is no develop branch.
View test results in the GitHub Actions tab of the repository.
We welcome contributions to the project. Please submit a pull request with clear documentation of any changes. All required checks above must pass before review. See CONTRIBUTING.md.
MIT License.