Versione: 2.0
Data: 2026-03-02
Autore: Claude Code / Antigravity
Path base: /root/projects/agents/
La Facebook Publishing Pipeline è un sistema multi-agente che trasforma un topic testuale in un post pubblicato sulla pagina Facebook di Galanti.digital. Il sistema è composto da cinque agenti Python specializzati orchestrati da n8n v2.
Input: un topic + parametri opzionali (tono, stile immagine) Output: post pubblicato su Facebook (testo + immagine) + notifica Telegram + log su Postgres
UTENTE / CLAUDE CODE
│
▼ POST webhook
┌──────────────────┐
│ n8n Webhook │ https://n8n-v2.m0t0.cloud/webhook/facebook-pipeline-start
│ Trigger │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ ResearchAgent │ :8603 — cerca info su Perplexity AI
│ /research │ → summary, sources, key_facts
└────────┬─────────┘
│ (fork parallelo)
┌────┴────┐
▼ ▼
┌──────────┐ ┌──────────────┐
│ Content │ │ ImageAgent │
│ Agent │ │ :8605 │
│ :8604 │ │ /generate │
│ /write │ │ → image_url │
│ → text │ └──────┬───────┘
│ hashtags │ │
│ cta │ │
└────┬─────┘ │
└─────────┬────┘
▼
┌──────────────────┐
│ ReviewAgent │ :8606 — verifica conformità persona
│ /review │ → approved, revised_text, violations
└────────┬─────────┘
│
┌────┴────┐
▼ ▼
[approved] [rejected]
│ │
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ Facebook │ │ Rejected Response │
│ Publisher │ │ (webhook 422) │
│ :8602 │ └──────────────────┘
│ /publish │
└──────┬──────┘
│ (fork parallelo)
┌────┴────┐
▼ ▼
Telegram Postgres
Notify Log
│ │
└────┬────┘
▼
Success Response (webhook 200)
Tutti gli agenti condividono:
- Framework: Agno 2.5.5
- Runtime: Python 3.12.3 in venv (/root/projects/agents/venv/)
- Modello primario: claude-sonnet-4-6 via LiteLLM proxy :4000
- Modello fast: claude-haiku-4-5 via LiteLLM proxy :4000
- Config: /root/.env.agents
- Persona: /root/projects/agents/shared/persona.md
File: specialists/research_agent.py
Modello: get_fast_model() → claude-haiku-4-5
Tool: PerplexityTool (API sonar, real-time web)
Endpoint:
POST /research
{
"topic": "string", # argomento da ricercare
"context": "string", # contesto aggiuntivo (opzionale)
"user_id": "giacomo"
}
Output:
{
"status": "ok",
"topic": "...",
"summary": "riassunto in 3-5 frasi in italiano",
"sources": ["url1", "url2"],
"key_facts": ["fatto 1", "fatto 2", "fatto 3"]
}
Logica interna:
1. Costruisce query: topic + contesto opzionale
2. Chiama Perplexity API (sonar) con sistema in italiano
3. Parsa il JSON dalla risposta dell'agente
4. Fallback: se il JSON non è parsabile, restituisce il testo raw come summary
Variabile d'ambiente richiesta: PERPLEXITY_API_KEY
File: specialists/content_agent.py
Modello: get_model() → claude-sonnet-4-6
Tool: FileTools() (accesso a file locali)
Endpoint:
POST /write
{
"topic": "string",
"research_brief": "string", # output summary da ResearchAgent
"tone": "professionale", # default
"persona_file": "/root/projects/agents/shared/persona.md",
"user_id": "giacomo"
}
Output:
{
"status": "ok",
"topic": "...",
"text": "corpo del post senza hashtag e senza emoji",
"hashtags": ["#hash1", "#hash2"],
"cta": "frase call-to-action"
}
Logica interna:
1. Carica persona.md e la inietta nelle istruzioni sistema dell'agente
2. Costruisce prompt con topic + tone + research_brief
3. Richiede JSON con campi text, hashtags, cta
4. Regole obbligatorie: max 3 paragrafi, frasi attive, no bullet point, no emoji nel testo
File: specialists/image_agent.py
Modello: nessuno (direct tool call, niente LLM)
Tool: FalTool (fal-ai/recraft-v3)
Endpoint:
POST /generate
{
"topic": "string",
"content_hint": "string", # hint visivo specifico (opzionale)
"style": "digital_illustration", # default recraft-v3
"image_size": "landscape_16_9", # default 1200x628 Facebook
"user_id": "giacomo"
}
Output:
{
"status": "ok",
"image_url": "https://...",
"prompt_used": "professional design, clean, minimal, brand identity, ...",
"style": "digital_illustration",
"model": "fal-ai/recraft-v3"
}
Logica interna:
1. Prepende brand prefix fisso: "professional design, clean, minimal, brand identity, "
2. Se content_hint è valorizzato, lo usa come base; altrimenti usa topic
3. Chiama fal.ai direttamente via httpx (niente LLM coinvolto)
4. Timeout: 90 secondi
Variabile d'ambiente richiesta: FAL_KEY
Stili recraft-v3 disponibili:
- digital_illustration (default)
- realistic_image
- vector_illustration
- square_hd, portrait_4_3, altri preset dimensionali
File: specialists/review_agent.py
Modello: get_fast_model() → claude-haiku-4-5
Tool: nessuno
Endpoint:
POST /review
{
"text": "string", # testo prodotto da ContentAgent
"image_url": "string", # URL immagine (opzionale)
"hashtags": ["#h1"], # hashtag da ContentAgent
"persona_file": "/root/projects/agents/shared/persona.md",
"user_id": "giacomo"
}
Output:
{
"status": "ok",
"approved": true,
"revised_text": "testo corretto o originale",
"revised_hashtags": ["#h1", "#h2"],
"violations": [],
"notes": "Nessuna modifica necessaria."
}
Checklist applicata: 1. Niente bullet point nel testo (-, *, •, numeri con punto) 2. Niente frasi passive ("viene usato", "è stato fatto") 3. Massimo 3 paragrafi 4. Nessuna emoji nel corpo del testo 5. Hashtag: massimo 5, tutti con # davanti 6. Tono: professionale ma accessibile 7. Call-to-action finale esplicita
Logica: se approved=false, il ReviewAgent corregge autonomamente il testo in revised_text.
File: specialists/facebook_publisher.py
Modello: nessuno (direct tool call)
Tool: FacebookTool (Meta Graph API v18.0)
Endpoint publish:
POST /publish
{
"text": "string", # testo finale (body + hashtags + emoji)
"image_url": "string", # URL immagine (opzionale)
"post_type": "photo|text", # "photo" se image_url presente
"link": "string", # URL da allegare (opzionale)
"user_id": "giacomo"
}
Endpoint dry-run:
POST /dry-run
{ stessi campi di /publish }
Output:
{
"status": "ok",
"dry_run": false,
"post_type": "photo",
"post_id": "123456789_987654321",
"text_published": "...",
"image_url": "https://...",
"timestamp": "2026-03-02T..."
}
Logica post_type:
- "photo" + image_url → POST /{page_id}/photos (foto con caption)
- "link" + link → POST /{page_id}/feed (link attachment)
- "text" → POST /{page_id}/feed (testo puro)
Variabili d'ambiente richieste:
- FACEBOOK_PAGE_ID (es. 2260289454212356)
- FACEBOOK_PAGE_ACCESS_TOKEN (Page Token, si rinnova ogni ~60 giorni)
- FACEBOOK_USER_TOKEN (fallback per derivare automaticamente il Page Token)
File: tools/perplexity_tool.py
Classe: PerplexityTool(Toolkit)
Modello API: sonar (Perplexity online, real-time)
Metodo esposto all'agente:
def search(self, query: str, focus: str = "internet") -> str
Restituisce JSON: {summary, sources, key_facts}
Configurazione API:
URL: https://api.perplexity.ai/chat/completions
Headers: Authorization: Bearer {PERPLEXITY_API_KEY}
max_tokens: 800, temperature: 0.2, return_citations: True
File: tools/fal_tool.py
Classe: FalTool(Toolkit)
Modello default: fal-ai/recraft-v3
Metodo esposto:
def generate_image(self, prompt, image_size=None, style=None, width=None, height=None) -> str
Restituisce JSON: {image_url, prompt_used, model, style, image_size}
Configurazione API:
URL: https://fal.run/fal-ai/recraft-v3
Headers: Authorization: Key {FAL_KEY}
timeout: 90s
File: tools/facebook_tool.py
Classe: FacebookTool(Toolkit)
API: Meta Graph API v18.0
Metodi registrati:
| Metodo | Descrizione |
|--------|-------------|
| get_page_info() | Info pagina (nome, ID, follower) |
| post_text(message) | Post testo puro |
| post_with_link(message, link) | Post con URL allegato |
| post_with_photo(photo_url, caption) | Foto con didascalia |
| delete_post(post_id) | Cancella post |
| list_recent_posts(limit=10) | Lista ultimi post |
Token resolution automatica: se FACEBOOK_PAGE_ACCESS_TOKEN non è impostato, il tool lo deriva da FACEBOOK_USER_TOKEN via Graph API.
Nota: i photo post NON sono editabili via Graph API. Solo i post testo sono modificabili.
Carica /root/.env.agents con python-dotenv. Espone:
| Costante | Default |
|---|---|
LITELLM_URL |
http://localhost:4000 |
LITELLM_MASTER_KEY |
sk-cosmo-master-key-2026 |
AGENTS_DIR |
/root/projects/agents |
MEMORY_DIR |
/root/projects/agents/memory |
DEFAULT_MODEL |
claude-sonnet-4-6 |
FALLBACK_MODEL |
qwen2.5:7b |
get_model(name=None) # default: claude-sonnet-4-6
get_fast_model() # claude-haiku-4-5
get_local_model() # qwen2.5:7b (Ollama fallback)
Tutti usano LiteLLMOpenAI(base_url=LITELLM_URL, api_key=LITELLM_MASTER_KEY).
Definisce il tone of voice di Galanti.digital. Viene iniettata nelle istruzioni di ContentAgent e ReviewAgent.
Strutture post supportate: - Post breve (fino a 150 parole): aggancio → affermazione → CTA - Post medio (150–350 parole): aggancio → sviluppo 2-3 paragrafi → CTA - Post lungo/divulgativo (200–250 parole): incipit → sviluppo → CTA
Regole universali: - Niente frasi passive - Frasi max 25 parole - Max 5 hashtag (2 branded + 2 tematici + 1 trending) - Emoji: max 2 (breve/medio), max 3 (divulgativo)
ID workflow: Q0M2xJVXi2AxFjxN
Nome: Facebook Publishing Pipeline v2
Webhook: https://n8n-v2.m0t0.cloud/webhook/facebook-pipeline-start
File: n8n_triggers/facebook-pipeline-v2.json
{
"topic": "Come l'AI sta cambiando il design",
"tone": "professionale",
"context": "focus su PMI italiane",
"image": true,
"image_style": "digital_illustration",
"image_hint": "AI and design tools visual concept",
"telegram_chat_id": "CHAT_ID"
}
| Campo | Tipo | Default | Note |
|---|---|---|---|
topic |
string | — | obbligatorio |
tone |
string | "professionale" |
Passato a ContentAgent |
context |
string | "" |
Contesto aggiuntivo per ricerca |
image |
bool | — | Non usato direttamente dal workflow (ImageAgent gira sempre) |
image_style |
string | "digital_illustration" |
Stile recraft-v3 |
image_hint |
string | topic | Prompt visivo alternativo per ImageAgent |
telegram_chat_id |
string | "" |
Chat ID per notifica (opzionale) |
| Nodo | Tipo | URL target | Timeout |
|---|---|---|---|
| Webhook Trigger | Webhook | — | — |
| Research Agent | HTTP Request | http://172.18.0.1:8603/research |
60s |
| Content Agent | HTTP Request | http://172.18.0.1:8604/write |
60s |
| Image Agent | HTTP Request | http://172.18.0.1:8605/generate |
120s |
| Review Agent | HTTP Request | http://172.18.0.1:8606/review |
60s |
| Approved? | IF | — | — |
| Facebook Publish | HTTP Request | http://172.18.0.1:8602/publish |
30s |
| Telegram Notify | Telegram | — | — |
| Postgres Log | Postgres | facebook_posts_log |
— |
| Success/Rejected Response | Respond to Webhook | — | — |
Nota sugli URL: n8n usa 172.18.0.1 (docker bridge gateway) per raggiungere i servizi sul host VPS. Il range localhost:86xx non è raggiungibile dall'interno del container.
{{ $('Review Agent').item.json.revised_text + '\n\n' + $('Content Agent').item.json.hashtags.join(' ') }}
Il testo finale unisce il corpo revisionato + gli hashtag separati da spazio.
| Servizio | Credential ID | Nome |
|---|---|---|
| Telegram | dvqA2GssfqHIAB2I |
Cosmo Operator |
| Postgres | VexUUB0wgLGeeSBy |
Postgres account (V4) |
-- Tabella: public.facebook_posts_log
CREATE TABLE IF NOT EXISTS facebook_posts_log (
id SERIAL PRIMARY KEY,
topic TEXT,
post_id TEXT,
text_published TEXT,
image_url TEXT,
approved BOOLEAN,
review_notes TEXT,
published_at TIMESTAMP
);
/root/scripts/start_agents.sh
Questo avvia in tmux dedicato:
| Sessione tmux | Agente | Porta |
|---|---|---|
cosmo-orchestrator |
CosmoOrchestrator | 8500 |
agent-builder |
AgentBuilder | 8501 |
facebook-publisher |
FacebookPublisher v3 | 8602 |
agent-researcher |
ResearchAgent | 8603 |
agent-content |
ContentAgent | 8604 |
agent-image |
ImageAgent | 8605 |
agent-reviewer |
ReviewAgent | 8606 |
/root/scripts/start_agents.sh --status
# oppure manualmente:
curl http://localhost:8602/health
curl http://localhost:8603/health
curl http://localhost:8604/health
curl http://localhost:8605/health
curl http://localhost:8606/health
/root/scripts/start_agents.sh --stop
# Research
curl -s -X POST http://localhost:8603/research \
-H "Content-Type: application/json" \
-d '{"topic": "AI nel design grafico", "context": ""}' | jq .
# Content
curl -s -X POST http://localhost:8604/write \
-H "Content-Type: application/json" \
-d '{"topic": "AI nel design", "research_brief": "...", "tone": "professionale"}' | jq .
# Image
curl -s -X POST http://localhost:8605/generate \
-H "Content-Type: application/json" \
-d '{"topic": "AI e design", "style": "digital_illustration"}' | jq .
# Review
curl -s -X POST http://localhost:8606/review \
-H "Content-Type: application/json" \
-d '{"text": "testo del post", "hashtags": ["#AI", "#design"]}' | jq .
# Dry-run publisher
curl -s -X POST http://localhost:8602/dry-run \
-H "Content-Type: application/json" \
-d '{"text": "Testo test.", "post_type": "text"}' | jq .
curl -s -X POST https://n8n-v2.m0t0.cloud/webhook/facebook-pipeline-start \
-H "Content-Type: application/json" \
-d '{
"topic": "AI nel branding italiano",
"tone": "professionale",
"image": true,
"image_style": "digital_illustration"
}' | jq .
File: /root/.env.agents
# LiteLLM
LITELLM_URL=http://localhost:4000
LITELLM_MASTER_KEY=sk-cosmo-master-key-2026
# AI APIs (via LiteLLM container — non modificare)
ANTHROPIC_API_KEY=...
OPENAI_API_KEY=...
# Perplexity
PERPLEXITY_API_KEY=pplx-...
# fal.ai
FAL_KEY=...
# Facebook
FACEBOOK_PAGE_ID=2260289454212356
FACEBOOK_PAGE_ACCESS_TOKEN=... # si rinnova ogni ~60 giorni
FACEBOOK_USER_TOKEN=... # fallback per derivare Page Token
# n8n
N8N_URL=https://n8n-v2.m0t0.cloud
N8N_API_KEY=...
# Modelli
DEFAULT_MODEL=claude-sonnet-4-6
FALLBACK_MODEL=qwen2.5:7b
Installate nel venv /root/projects/agents/venv/:
agno==2.5.5
anthropic
openai
fastapi
uvicorn
httpx
pydantic
python-dotenv
rich
litellm
sqlalchemy
Installazione da zero:
cd /root/projects/agents
python3 -m venv venv
source venv/bin/activate
pip install agno anthropic openai fastapi uvicorn httpx pydantic python-dotenv rich litellm sqlalchemy
| API obsoleta | Sostituzione |
|---|---|
SqliteAgentStorage |
agno.db.sqlite.SqliteDb con db= |
SqliteMemoryDb |
idem |
RunResponse |
RunOutput |
LiteLLM(api_base=) |
LiteLLMOpenAI(base_url=) per proxy locale |
Bug noto: Agno invia temperature + top_p insieme ad Anthropic → errore 400.
Fix in shared/llm.py: non passare top_p per modelli Claude.
POST /{post_id}?message=....env.agentsI nodi n8n usano 172.18.0.1 (docker bridge gateway) per raggiungere i servizi sul host. La porta localhost:86xx non è raggiungibile dall'interno del container n8n. Il firewall UFW blocca attualmente le connessioni da 172.18.0.1 — se il workflow fallisce, verificare le regole UFW.
sonar: real-time web, restituisce citations arrayPERPLEXITY_API_KEY non è configurata, il ResearchAgent restituisce errore JSON pulito/root/projects/agents/
├── venv/ # Python 3.12.3 virtualenv
├── shared/
│ ├── config.py # Costanti, carica .env.agents
│ ├── llm.py # get_model() / get_fast_model()
│ ├── logging.py # Logger rich
│ ├── persona.md # Tone of voice Galanti.digital
│ └── registry.md # Registro agenti attivi
├── specialists/
│ ├── research_agent.py # :8603 — Perplexity web research
│ ├── content_agent.py # :8604 — Facebook post writer
│ ├── image_agent.py # :8605 — fal.ai image generation
│ ├── review_agent.py # :8606 — Content QA / enforcement
│ └── facebook_publisher.py # :8602 — Meta Graph API publisher (v3 slim)
├── tools/
│ ├── perplexity_tool.py # Agno Toolkit — Perplexity API
│ ├── fal_tool.py # Agno Toolkit — fal.ai recraft-v3
│ ├── facebook_tool.py # Agno Toolkit — Meta Graph API v18.0
│ ├── tmux_tool.py # Agno Toolkit — tmux sessions
│ └── n8n_api_tool.py # Agno Toolkit — n8n v2 API
├── meta/
│ └── agent_builder.py # AgentBuilder meta-agent :8501
├── orchestrators/
│ └── cosmo_orchestrator.py # CosmoOrchestrator :8500
├── memory/ # SQLite DB per sessioni agenti
│ ├── research_agent.db
│ ├── content_agent.db
│ └── review_agent.db
├── n8n_triggers/
│ └── facebook-pipeline-v2.json # Workflow n8n importabile
└── specialists/
└── ...
curl -X POST https://n8n-v2.m0t0.cloud/webhook/facebook-pipeline-start \
-H "Content-Type: application/json" \
-d '{
"topic": "ARGOMENTO DEL POST",
"tone": "professionale",
"image_style": "digital_illustration",
"telegram_chat_id": "IL_TUO_CHAT_ID"
}'
# 1. Research
RESEARCH=$(curl -s -X POST http://localhost:8603/research \
-H "Content-Type: application/json" \
-d '{"topic": "ARGOMENTO"}')
SUMMARY=$(echo $RESEARCH | jq -r '.summary')
# 2. Content (parallelo a image)
CONTENT=$(curl -s -X POST http://localhost:8604/write \
-H "Content-Type: application/json" \
-d "{\"topic\": \"ARGOMENTO\", \"research_brief\": \"$SUMMARY\", \"tone\": \"professionale\"}")
# 3. Image (in parallelo)
IMAGE=$(curl -s -X POST http://localhost:8605/generate \
-H "Content-Type: application/json" \
-d '{"topic": "ARGOMENTO", "style": "digital_illustration"}')
# 4. Review
TEXT=$(echo $CONTENT | jq -r '.text')
HASHTAGS=$(echo $CONTENT | jq '.hashtags')
IMAGE_URL=$(echo $IMAGE | jq -r '.image_url')
REVIEW=$(curl -s -X POST http://localhost:8606/review \
-H "Content-Type: application/json" \
-d "{\"text\": \"$TEXT\", \"hashtags\": $HASHTAGS, \"image_url\": \"$IMAGE_URL\"}")
# 5. Pubblica se approvato
APPROVED=$(echo $REVIEW | jq -r '.approved')
if [ "$APPROVED" = "true" ]; then
REVISED=$(echo $REVIEW | jq -r '.revised_text')
FINAL_TEXT="$REVISED\n\n$(echo $HASHTAGS | jq -r 'join(" ")')"
curl -s -X POST http://localhost:8602/publish \
-H "Content-Type: application/json" \
-d "{\"text\": \"$FINAL_TEXT\", \"image_url\": \"$IMAGE_URL\", \"post_type\": \"photo\"}"
fi