Add 3-day/7-day multi-window analysis for trend-based decisions
- Fetch campaign metrics for 1d/3d/7d windows at run start (2 extra API calls total) - Pass spend_3d/leads_3d/cpl_3d and spend_7d/leads_7d/cpl_7d to Claude decide() - DECIDE_SYSTEM: decisions based on cpl_3d (primary) vs cpl_7d (trend direction) - PAUSE rule now uses leads_3d==0 + spend_3d >= 3x max_cpl threshold - Adset and ad analysis now use 3-day aggregated data (get_period_*_metrics) - Add get_period_campaign_metrics/adset/ad methods to MetaAdsClient - Print shows 1d/3d/7d CPL and leads per campaign for easy trend reading - UNIT_SYSTEM and AD_SYSTEM note data covers last 3 days Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8b6ec106ee
commit
6ec3342661
21
agent.py
21
agent.py
@ -11,12 +11,19 @@ Eres un experto en optimización de campañas de Meta Ads para generación de le
|
|||||||
Cada campaña tiene un CPL máximo (coste por lead objetivo) que define el límite aceptable.
|
Cada campaña tiene un CPL máximo (coste por lead objetivo) que define el límite aceptable.
|
||||||
USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español.
|
USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español.
|
||||||
|
|
||||||
REGLAS DE DECISIÓN:
|
Recibirás métricas en tres ventanas temporales:
|
||||||
1. CPL > max_cpl → REDUCE_BUDGET (nunca PAUSE por CPL alto).
|
- spend/leads/cpl: ayer (1 día, contexto inmediato)
|
||||||
2. CPL <= max_cpl con bajo volumen → INCREASE_BUDGET si hay margen.
|
- spend_3d/leads_3d/cpl_3d: últimos 3 días (ventana PRINCIPAL para decisiones)
|
||||||
3. Frecuencia > 3.0 → considera rotar creatividades o ampliar audiencia.
|
- spend_7d/leads_7d/cpl_7d: últimos 7 días (tendencia de fondo)
|
||||||
4. CTR < 1% → problema de creatividad, revisar anuncios.
|
|
||||||
5. PAUSE solo si: leads == 0 Y gasto >= 3 × max_cpl (fallo técnico grave de tracking/píxel). En cualquier otro caso de bajo rendimiento usa REDUCE_BUDGET, nunca PAUSE.
|
REGLAS DE DECISIÓN (basadas principalmente en cpl_3d y leads_3d):
|
||||||
|
1. cpl_3d > max_cpl → REDUCE_BUDGET (nunca PAUSE por CPL alto).
|
||||||
|
2. cpl_3d <= max_cpl con bajo volumen → INCREASE_BUDGET si hay margen.
|
||||||
|
3. Si cpl_3d < cpl_7d → tendencia mejorando: más permisivo con INCREASE_BUDGET.
|
||||||
|
4. Si cpl_3d > cpl_7d × 1.2 → tendencia empeorando: más estricto, considera REDUCE_BUDGET.
|
||||||
|
5. CTR < 1% → problema de creatividad, revisar anuncios.
|
||||||
|
6. PAUSE solo si: leads_3d == 0 Y spend_3d >= 3 × max_cpl (fallo técnico grave de tracking/píxel).
|
||||||
|
En cualquier otro caso de bajo rendimiento usa REDUCE_BUDGET, nunca PAUSE.
|
||||||
|
|
||||||
Responde SOLO con JSON válido, sin texto adicional ni markdown:
|
Responde SOLO con JSON válido, sin texto adicional ni markdown:
|
||||||
{
|
{
|
||||||
@ -67,6 +74,7 @@ def decide(analysis: dict) -> dict:
|
|||||||
|
|
||||||
UNIT_SYSTEM = """
|
UNIT_SYSTEM = """
|
||||||
Eres un analista experto en Meta Ads. Analiza las métricas del conjunto de anuncios indicado.
|
Eres un analista experto en Meta Ads. Analiza las métricas del conjunto de anuncios indicado.
|
||||||
|
Los datos corresponden a los últimos 3 días (ventana estándar de análisis).
|
||||||
USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español.
|
USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español.
|
||||||
Si el conjunto tiene cost_cap_eur (cap de coste), compara el CPL actual con ese cap e indica si está
|
Si el conjunto tiene cost_cap_eur (cap de coste), compara el CPL actual con ese cap e indica si está
|
||||||
por encima, dentro o por debajo del límite, y cuánto margen queda (o cuánto se supera).
|
por encima, dentro o por debajo del límite, y cuánto margen queda (o cuánto se supera).
|
||||||
@ -76,6 +84,7 @@ Responde SOLO con JSON válido (sin markdown):
|
|||||||
|
|
||||||
AD_SYSTEM = """
|
AD_SYSTEM = """
|
||||||
Eres un analista experto en Meta Ads. Analiza las métricas del anuncio indicado.
|
Eres un analista experto en Meta Ads. Analiza las métricas del anuncio indicado.
|
||||||
|
Los datos corresponden a los últimos 3 días (ventana estándar de análisis).
|
||||||
USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español.
|
USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español.
|
||||||
Responde SOLO con JSON válido (sin markdown):
|
Responde SOLO con JSON válido (sin markdown):
|
||||||
{"evaluacion": "resumen del rendimiento en 2 frases usando €", "recomendacion": "una acción concreta", "accion": "PAUSE o MAINTAIN"}
|
{"evaluacion": "resumen del rendimiento en 2 frases usando €", "recomendacion": "una acción concreta", "accion": "PAUSE o MAINTAIN"}
|
||||||
|
|||||||
@ -168,6 +168,24 @@ class MetaAdsClient:
|
|||||||
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
return self.get_ad_metrics(campaign_id, yesterday, yesterday)
|
return self.get_ad_metrics(campaign_id, yesterday, yesterday)
|
||||||
|
|
||||||
|
def get_period_campaign_metrics(self, days: int) -> dict:
|
||||||
|
today = datetime.now()
|
||||||
|
date_to = (today - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
date_from = (today - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||||
|
return self.get_campaign_metrics(date_from, date_to)
|
||||||
|
|
||||||
|
def get_period_adset_metrics(self, campaign_id: str, days: int) -> list:
|
||||||
|
today = datetime.now()
|
||||||
|
date_to = (today - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
date_from = (today - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||||
|
return self.get_adset_metrics(campaign_id, date_from, date_to)
|
||||||
|
|
||||||
|
def get_period_ad_metrics(self, campaign_id: str, days: int) -> list:
|
||||||
|
today = datetime.now()
|
||||||
|
date_to = (today - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
date_from = (today - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||||
|
return self.get_ad_metrics(campaign_id, date_from, date_to)
|
||||||
|
|
||||||
def get_ads_with_creatives(self, campaign_id: str) -> list:
|
def get_ads_with_creatives(self, campaign_id: str) -> list:
|
||||||
"""
|
"""
|
||||||
Returns active ads for a campaign with their thumbnail URLs for creative analysis.
|
Returns active ads for a campaign with their thumbnail URLs for creative analysis.
|
||||||
|
|||||||
24
run.py
24
run.py
@ -145,6 +145,12 @@ def run():
|
|||||||
metrics_all = meta.get_yesterday_metrics()
|
metrics_all = meta.get_yesterday_metrics()
|
||||||
print(f" ✓ {len(metrics_all)} campaigns active yesterday.\n")
|
print(f" ✓ {len(metrics_all)} campaigns active yesterday.\n")
|
||||||
|
|
||||||
|
# ── 3-day and 7-day metrics for trend analysis ────────────────────────────
|
||||||
|
print(f"→ Fetching 3-day and 7-day metrics...")
|
||||||
|
metrics_3d = meta.get_period_campaign_metrics(days=3)
|
||||||
|
metrics_7d = meta.get_period_campaign_metrics(days=7)
|
||||||
|
print(f" ✓ Multi-window data ready.\n")
|
||||||
|
|
||||||
# ── Analyze campaigns & propose actions ───────────────────────────────────
|
# ── Analyze campaigns & propose actions ───────────────────────────────────
|
||||||
actions_proposed_list = []
|
actions_proposed_list = []
|
||||||
campaign_details = {} # {cid: {vertical, margin, adsets, ads}}
|
campaign_details = {} # {cid: {vertical, margin, adsets, ads}}
|
||||||
@ -156,10 +162,13 @@ def run():
|
|||||||
max_cpl = vertical_cpls.get(vertical, config.META_TARGET_CPL) or config.META_TARGET_CPL
|
max_cpl = vertical_cpls.get(vertical, config.META_TARGET_CPL) or config.META_TARGET_CPL
|
||||||
margin = round((max_cpl - metrics["cpl"]) * metrics["leads"], 2) if metrics["leads"] > 0 else round(-metrics["spend"], 2)
|
margin = round((max_cpl - metrics["cpl"]) * metrics["leads"], 2) if metrics["leads"] > 0 else round(-metrics["spend"], 2)
|
||||||
|
|
||||||
|
m3 = metrics_3d.get(cid, {})
|
||||||
|
m7 = metrics_7d.get(cid, {})
|
||||||
analysis = {
|
analysis = {
|
||||||
"campaign_id": cid,
|
"campaign_id": cid,
|
||||||
"name": metrics["name"],
|
"name": metrics["name"],
|
||||||
"status": metrics["status"],
|
"status": metrics["status"],
|
||||||
|
# Yesterday (1d) — contexto inmediato
|
||||||
"spend": metrics["spend"],
|
"spend": metrics["spend"],
|
||||||
"leads": metrics["leads"],
|
"leads": metrics["leads"],
|
||||||
"cpl": metrics["cpl"],
|
"cpl": metrics["cpl"],
|
||||||
@ -168,6 +177,14 @@ def run():
|
|||||||
"cpm": metrics["cpm"],
|
"cpm": metrics["cpm"],
|
||||||
"impressions": metrics["impressions"],
|
"impressions": metrics["impressions"],
|
||||||
"clicks": metrics["clicks"],
|
"clicks": metrics["clicks"],
|
||||||
|
# 3 días — ventana principal para decisiones
|
||||||
|
"spend_3d": m3.get("spend", 0.0),
|
||||||
|
"leads_3d": m3.get("leads", 0),
|
||||||
|
"cpl_3d": m3.get("cpl", 0.0),
|
||||||
|
# 7 días — tendencia de fondo
|
||||||
|
"spend_7d": m7.get("spend", 0.0),
|
||||||
|
"leads_7d": m7.get("leads", 0),
|
||||||
|
"cpl_7d": m7.get("cpl", 0.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -200,7 +217,8 @@ def run():
|
|||||||
action_type = "MAINTAIN"
|
action_type = "MAINTAIN"
|
||||||
|
|
||||||
print(f" {metrics['name'][:52]}")
|
print(f" {metrics['name'][:52]}")
|
||||||
print(f" Spend: {metrics['spend']}€ Leads: {metrics['leads']} CPL: {metrics['cpl']}€ MaxCPL: {max_cpl}€ Margen: {margin:+.2f}€")
|
print(f" CPL: 1d={metrics['cpl']}€ / 3d={m3.get('cpl',0.0):.2f}€ / 7d={m7.get('cpl',0.0):.2f}€ MaxCPL: {max_cpl}€ Margen: {margin:+.2f}€")
|
||||||
|
print(f" Leads: 1d={metrics['leads']} / 3d={m3.get('leads',0)} / 7d={m7.get('leads',0)} Spend 1d: {metrics['spend']}€")
|
||||||
print(f" Vertical: {vertical} Decision: {action_type} — {(decision.get('justification') or '')[:70]}")
|
print(f" Vertical: {vertical} Decision: {action_type} — {(decision.get('justification') or '')[:70]}")
|
||||||
if decision.get("alert"):
|
if decision.get("alert"):
|
||||||
print(f" ALERT: {decision['alert']}")
|
print(f" ALERT: {decision['alert']}")
|
||||||
@ -236,7 +254,7 @@ def run():
|
|||||||
# ── Ad set analysis ────────────────────────────────────────────────
|
# ── Ad set analysis ────────────────────────────────────────────────
|
||||||
adsets_detail = []
|
adsets_detail = []
|
||||||
try:
|
try:
|
||||||
for as_m in meta.get_yesterday_adset_metrics(cid)[:5]:
|
for as_m in meta.get_period_adset_metrics(cid, days=3)[:5]:
|
||||||
bid = adset_bids.get(as_m["id"], {})
|
bid = adset_bids.get(as_m["id"], {})
|
||||||
as_m["bid_strategy"] = bid.get("bid_strategy", "")
|
as_m["bid_strategy"] = bid.get("bid_strategy", "")
|
||||||
as_m["cost_cap_eur"] = bid.get("cost_cap_eur")
|
as_m["cost_cap_eur"] = bid.get("cost_cap_eur")
|
||||||
@ -249,7 +267,7 @@ def run():
|
|||||||
# ── Ad analysis ────────────────────────────────────────────────────
|
# ── Ad analysis ────────────────────────────────────────────────────
|
||||||
ads_detail = []
|
ads_detail = []
|
||||||
try:
|
try:
|
||||||
for ad_m in meta.get_yesterday_ad_metrics(cid)[:5]:
|
for ad_m in meta.get_period_ad_metrics(cid, days=3)[:5]:
|
||||||
ad_m["max_cpl"] = max_cpl
|
ad_m["max_cpl"] = max_cpl
|
||||||
result = analyze_unit(ad_m, "ad")
|
result = analyze_unit(ad_m, "ad")
|
||||||
ad_entry = {**ad_m, **result}
|
ad_entry = {**ad_m, **result}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user