SocialPilot 社媒内容生成与审批工作流

SocialPilot 是一个基于 LangGraph 和 CrewAI 的社交媒体内容自动化工作流:它按任务类型生成社媒内容,在涉及禁用话题时自动暂停并转人工审批,通过 API 或仪表盘通过后进入排程,适用于需要内容审核与合规控制的社交媒体运营场景。

  • by daniellopez882
  • Python
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 daniellopez882/Social-Media-Autopilot-Agent-with-MCP

使用说明

SocialPilot

A LangGraph workflow that drafts social-media content for a brand, holds it for a human when it names a banned topic, and hands approved content to a scheduling step. Agents are crewai; the API is FastAPI; the dashboard is one page served by the same process.

At a glance

Does Route a task to the right agents, generate content (mock or model), hold it on a whole-word banned-topic match, queue it for approval, list/approve/reject through an authenticated API and a dashboard
Does not Talk to any social platform. All four platform tools return status: "not_implemented" outside mock mode. Nothing is fetched or posted anywhere
Is not An MCP server or client. The repository was named Social-Media-Autopilot-Agent-with-MCP and was renamed because it contains no MCP code
Tests 189 — none reach a network or need a credential
CI lint · tests on 3.11/3.12 · server booted and its contract exercised · bandit · container built, run, and checked
Container non-root (uid 10001), multi-stage, refuses an invalid configuration at startup
Live model path tested with fakes and by construction against the real crewai; not run against the OpenAI API here

Architecture

flowchart LR
    RUN[POST /run] --> ORC{orchestrator<br/>task_type}
    ORC -->|campaign| TR[trend_analyzer]
    ORC -->|content| CG[content_generator]
    ORC -->|engagement| ER[engagement_responder] --> E1((end))
    ORC -->|analytics| AR[analytics_reporter] --> E2((end))
    TR --> CG
    CG --> GR{guardrails<br/>whole-word match<br/>on banned topics}
    GR -->|clear| SC[campaign_scheduler] --> E3((end))
    GR -->|held| Q[(pending approvals<br/>TTL + bound)]
    Q -->|POST /approve/id| SC
    Q -->|POST /reject/id| DROP((dropped))
    classDef guard fill:#f59e0b,color:#111,stroke:#b45309
    classDef store fill:#334155,color:#fff,stroke:#0f172a
    class GR guard
    class Q store

scheduling is how an approved item re-enters the graph. It is not a public task type, because it bypasses guardrails.

A held item, end to end

sequenceDiagram
    autonumber
    participant Op as Operator (dashboard)
    participant API as FastAPI
    participant G as LangGraph
    participant Q as ApprovalStore

    Op->>API: POST /client/profile/{id}  (X-API-Key)
    API-->>Op: 200 Profile saved
    Op->>API: POST /run {client_id, task_type: content}
    API->>G: invoke(state)
    G->>G: content_generator → guardrails
    G-->>API: requires_human_approval = true
    API->>Q: add(reason, content, state)
    API-->>Op: 200 {status: awaiting_approval, approval_id, reason, mock_mode}
    Op->>API: GET /approvals
    API-->>Op: [{approval_id, client_id, reason, content, created_at}]
    Op->>API: POST /approve/{approval_id}
    API->>Q: pop(approval_id)
    API->>G: invoke(state, task_type = scheduling)
    G-->>API: scheduling_status
    API-->>Op: 200 Content approved and sent to scheduling

Quick start

python -m venv .venv && . .venv/bin/activate     # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env

To run without a model key, say so explicitly — mock mode is off unless you turn it on:

MOCK_MODE=true API_KEY=dev uvicorn app.main:app --reload

Open http://127.0.0.1:8000/, paste the key into the sidebar, register a profile, run a task. Every response carries mock_mode, and every mock value is prefixed [MOCK].

For live mode set OPENAI_API_KEY and leave MOCK_MODE unset. Preflight (python -m app.preflight, also run at startup) refuses to serve production with mock mode on or with no API_KEY.

Container

docker build -t socialpilot .
docker run --rm -p 8000:8000 --env-file .env socialpilot

The container exits non-zero on an invalid configuration rather than serving.

Configuration

Every setting, with its default, is in .env.example.

Variable Default Notes
MOCK_MODE false Labelled mock data everywhere; refused in production. A blank value is "off"
API_KEY (empty) Required by every route that reads or changes state. Empty is allowed in development only
OPENAI_API_KEY / OPENAI_MODEL — / gpt-4o The only wired provider
TWITTER_API_KEY, META_ACCESS_TOKEN, BUFFER_ACCESS_TOKEN (empty) No live integration uses them yet; tools report not_implemented either way
APPROVAL_TTL_SECONDS / APPROVAL_MAX_PENDING 86400 / 1000 The in-memory approval queue expires and is bounded
CORS_ALLOW_ORIGINS (empty) Same-origin by default; never a wildcard

API

Every route except /health, /ready and / requires X-API-Key.

Route Purpose
POST /client/profile/{client_id} Save a brand profile — client_id matches [A-Za-z0-9][A-Za-z0-9_-]{0,63}
GET /client/profiles · GET /client/profile/{client_id} List · read
POST /run {"client_id", "task_type": campaign | content | engagement | analytics}
GET /approvals Pending items, oldest first
POST /approve/{id} Send held content to scheduling
POST /reject/{id} {"feedback": "..."}
GET /health · GET /ready Liveness · readiness with per-check detail

Security model

flowchart TB
    subgraph trusted [Trusted]
        OP[Operator with API key]
        ENV[.env: keys, tokens]
    end
    subgraph process [This process]
        API[FastAPI + dashboard at /]
        GRAPH[LangGraph nodes]
        STORE[(in-memory profiles + approvals)]
    end
    subgraph external [Not yet connected]
        X[X / Google Trends / Meta / Buffer]
    end
    OP -->|X-API-Key, constant-time| API
    ENV --> API
    API --> GRAPH --> STORE
    GRAPH -. not_implemented .-> X
  • Every state change needs the key; approval ids are 128-bit and still need the key.
  • The dashboard is served by the API — the old separate server exposed the repository root over HTTP.
  • Errors return a request id, never the exception text.
  • No third-party text reaches a prompt today, only because the tools that would fetch it are not implemented. The threat model records what changes when one lands.

What changed, and why

This began as a prototype whose success signals were unconditional. Every defect below was reproduced on the original code before it was fixed.

# Defect Effect
1 MOCK_MODE defaulted to "true" and was read once at import A fake system by default; setting the variable later changed nothing
2 except ImportError: MOCK_MODE = True crewai declares <3.14; on 3.14 the app silently became a mock system
3 buffer_post_scheduler returned "LIVE: Scheduled …" with a token set No request was made — worse with credentials than without
4 instagram_analytics_fetcher returned reach: 12000 with a token set A fabricated metric presented as live data
5 Guardrails used substring containment Banning art held "start … smart"; banning X held everything
6 The content node appended the brand's first banned topic "for testing" Every brand with a banned list was escalated on every run — the only reason the old test passed
7 No authentication on any route Anyone reaching the port could overwrite any profile or approve content by UUID
8 run_dashboard.py served the repository root /app/main.py, /requirements.txt, /.env.example returned 200
9 Hardcoded "Weekly Growth +12.4%", "14.2K Total Accounts Reached" in the page Fabricated metrics styled as live
10 Unknown task_type routed to END POST /run with "nonsense" returned 200 and the untouched state
Ten more
# Defect
11 allow_origins=["*"] with allow_credentials=True, only because the page lived on another port
12 The graph was recompiled on every request
13 detail=str(e) on every 500
14 No GET /approvals; dict stores with no TTL or bound — a page reload orphaned held items forever
15 A failed ChatOpenAI construction was swallowed into llm=None; the model name was hardcoded
16 A LangChain model object was handed to crewai, which rebuilds its own client from os.environ and cannot see a key that lives only in .env
17 LangChain tools assigned to a crewai agent after construction; crewai's converter is broken in 1.15.20
18 CrewOutput objects stored in Optional[str] fields → not JSON-serialisable → 500 in live mode
19 requirements.txt unpinned; sqlalchemy and langchain-anthropic imported by nothing
20 while True: pass in the dashboard server — one core pinned

Design notes

Record Decision
ADR 0001 Mock mode is opt-in, read at call time, refused in production
ADR 0002 A tool that is not implemented says so
ADR 0003 Guardrails match whole words; production code carries no test hooks
ADR 0004 One process serves the API and the dashboard; every state change needs a key
ADR 0005 crewai gets its own LLM with an explicit key; LangChain tools are adapted
Threat model Assets, boundaries, eight threats, what is not addressed

Layout

app/
  main.py            routes, auth, lifespan (preflight + graph compile)
  config.py          settings; MOCK_MODE default False
  preflight.py       python -m app.preflight
  guardrails.py      whole-word banned-topic matching
  approvals.py       in-memory pending approvals, TTL + bound
  graphs/            nodes.py, orchestrator.py
  agents/factory.py  crewai agents; LLM with an explicit key
  tools/             the four platform tools (not implemented) + crewai adapters
social_media_prompts.py   the agent prompts
index.html           the dashboard, served at /
tests/               189 tests
docs/                ADRs, threat model

Limits

  • State is process memory. A restart loses profiles and pending approvals; replicas would not share them.
  • One shared API key. The dashboard keeps it in sessionStorage for the tab.
  • No platform integration exists — see ADR 0002.
  • No latency or cost figures: nothing here has been measured against a model.

Licence

MIT — see LICENSE.