Fix ad analysis: exclude paused ads, dual 3d/7d window, clearer Slack labels
- meta_ads_client: filter currently-paused ads from analysis (they have historic spend in the window but shouldn't get PAUSE recommendations) - run.py: fetch both 3d and 7d ad metrics; merge cpl_3d/cpl_7d into each ad - agent.py AD_SYSTEM: base PAUSE on 7d window (more stable for low-volume ads); treat high cpl_3d with acceptable cpl_7d as statistical noise - slack_notifier: campaign header shows yesterday's spend/leads explicitly; ad table shows CPL(3d) and CPL(7d) side by side; labels include time period Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
184b9ca4a5
commit
c46baad502
13
agent.py
13
agent.py
@ -84,11 +84,18 @@ Responde SOLO con JSON válido (sin markdown):
|
||||
|
||||
AD_SYSTEM = """
|
||||
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).
|
||||
Los datos incluyen dos ventanas temporales:
|
||||
- cpl_3d / leads_3d / spend_3d: últimos 3 días (puede ser volátil con poco volumen)
|
||||
- cpl_7d / leads_7d: últimos 7 días (más estable, úsala como referencia principal)
|
||||
Los campos spend/leads/cpl sin sufijo corresponden a 7 días.
|
||||
USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español.
|
||||
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"}
|
||||
Usa "accion": "PAUSE" solo si el gasto supera 3 veces el CPL objetivo (campo max_cpl en los datos; si no existe, usa 15€) con 0 conversiones, o si el CPL supera el doble del objetivo Y el gasto ya alcanza 3 veces el CPL objetivo.
|
||||
{"evaluacion": "resumen del rendimiento en 2 frases usando €, mencionando diferencia 3d/7d si es relevante", "recomendacion": "una acción concreta", "accion": "PAUSE o MAINTAIN"}
|
||||
|
||||
Reglas para "accion": "PAUSE":
|
||||
- SOLO si leads_7d == 0 Y spend de 7 días supera 3 veces el max_cpl (o 15€ si no existe max_cpl).
|
||||
- Si cpl_3d es alto pero cpl_7d está dentro del objetivo, usa "MAINTAIN" (ruido estadístico de período corto).
|
||||
- Si el anuncio tiene leads en 7 días aunque el CPL sea alto, usa "MAINTAIN" y recomienda optimizar.
|
||||
En cualquier otro caso usa "accion": "MAINTAIN".
|
||||
"""
|
||||
|
||||
|
||||
@ -146,6 +146,18 @@ class MetaAdsClient:
|
||||
"leads": int(leads),
|
||||
"cpl": cpl,
|
||||
})
|
||||
# For ads, exclude currently paused ads (they may have historic spend in the window)
|
||||
if level == "ad":
|
||||
try:
|
||||
active_ads = Campaign(campaign_id).get_ads(
|
||||
fields=["id"],
|
||||
params={"effective_status": ["ACTIVE"]},
|
||||
)
|
||||
active_ids = {a["id"] for a in active_ads}
|
||||
result = [r for r in result if r["id"] in active_ids]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return sorted(result, key=lambda x: -x["spend"])
|
||||
|
||||
def get_yesterday_metrics(self) -> dict:
|
||||
|
||||
22
run.py
22
run.py
@ -271,11 +271,25 @@ def run():
|
||||
except Exception as e:
|
||||
errors.append(f"Adsets {metrics['name']}: {e}")
|
||||
|
||||
# ── Ad analysis ────────────────────────────────────────────────────
|
||||
# ── Ad analysis (3d + 7d merged) ───────────────────────────────────
|
||||
ads_detail = []
|
||||
try:
|
||||
for ad_m in meta.get_period_ad_metrics(cid, days=3)[:5]:
|
||||
ad_m["max_cpl"] = max_cpl
|
||||
ads_3d = {a["id"]: a for a in meta.get_period_ad_metrics(cid, days=3)}
|
||||
ads_7d = {a["id"]: a for a in meta.get_period_ad_metrics(cid, days=7)}
|
||||
ordered_ids = list(dict.fromkeys(
|
||||
[a["id"] for a in sorted(ads_7d.values(), key=lambda x: -x["spend"])] +
|
||||
[a["id"] for a in sorted(ads_3d.values(), key=lambda x: -x["spend"])]
|
||||
))[:5]
|
||||
for ad_id in ordered_ids:
|
||||
a3 = ads_3d.get(ad_id, {})
|
||||
a7 = ads_7d.get(ad_id, {})
|
||||
ad_m = dict(a7) if a7 else dict(a3)
|
||||
ad_m["cpl_3d"] = a3.get("cpl", 0.0)
|
||||
ad_m["leads_3d"] = a3.get("leads", 0)
|
||||
ad_m["spend_3d"] = a3.get("spend", 0.0)
|
||||
ad_m["cpl_7d"] = a7.get("cpl", 0.0)
|
||||
ad_m["leads_7d"] = a7.get("leads", 0)
|
||||
ad_m["max_cpl"] = max_cpl
|
||||
result = analyze_unit(ad_m, "ad")
|
||||
ad_entry = {**ad_m, **result}
|
||||
# Propose ad pause if Claude recommends it
|
||||
@ -303,6 +317,8 @@ def run():
|
||||
"name": metrics["name"],
|
||||
"vertical": vertical,
|
||||
"margin": margin,
|
||||
"spend_1d": metrics["spend"],
|
||||
"leads_1d": metrics["leads"],
|
||||
"adsets": adsets_detail,
|
||||
"ads": ads_detail,
|
||||
"bid_config": campaign_bid,
|
||||
|
||||
@ -121,11 +121,13 @@ def main():
|
||||
ads = []
|
||||
|
||||
campaign_details[cid] = {
|
||||
"name": name,
|
||||
"name": name,
|
||||
"vertical": vertical,
|
||||
"margin": margin,
|
||||
"adsets": adsets,
|
||||
"ads": ads,
|
||||
"margin": margin,
|
||||
"spend_1d": spend,
|
||||
"leads_1d": leads,
|
||||
"adsets": adsets,
|
||||
"ads": ads,
|
||||
"bid_config": {},
|
||||
}
|
||||
metrics_all[cid] = {"name": name, "spend": spend, "leads": leads, "cpl": cpl}
|
||||
|
||||
@ -105,13 +105,16 @@ def _ad_action_blocks(ads: list) -> list:
|
||||
return blocks
|
||||
|
||||
|
||||
def _adset_ad_table(items: list, label: str, show_bid: bool = False) -> str:
|
||||
def _adset_ad_table(items: list, label: str, show_bid: bool = False, show_7d: bool = False) -> str:
|
||||
"""Genera tabla monoespaciada de adsets o anuncios para Slack."""
|
||||
if not items:
|
||||
return ""
|
||||
lines = [f"*{label}*"]
|
||||
lines.append("```")
|
||||
if show_bid:
|
||||
if show_7d:
|
||||
lines.append(f"{'Nombre':<45} {'Gasto':>6} {'Leads':>5} {'CPL(3d)':>8} {'CPL(7d)':>8} {'CTR':>5}")
|
||||
lines.append("─" * 82)
|
||||
elif show_bid:
|
||||
lines.append(f"{'Nombre':<45} {'Gasto':>6} {'Leads':>5} {'CPL':>6} {'CTR':>5} {'Cap':>7}")
|
||||
lines.append("─" * 79)
|
||||
else:
|
||||
@ -120,15 +123,26 @@ def _adset_ad_table(items: list, label: str, show_bid: bool = False) -> str:
|
||||
for it in items:
|
||||
name = _table_name(it["name"], 45)
|
||||
leads_str = f"{it['leads']:>5}" if it["leads"] > 0 else " —"
|
||||
cpl_str = f"{it['cpl']:>5.2f}€" if it["leads"] > 0 else " —"
|
||||
if show_bid:
|
||||
if show_7d:
|
||||
cpl_3d = it.get("cpl_3d", 0.0)
|
||||
cpl_7d = it.get("cpl_7d", it.get("cpl", 0.0))
|
||||
cpl_3d_str = f"{cpl_3d:>6.2f}€" if cpl_3d > 0 else " —"
|
||||
cpl_7d_str = f"{cpl_7d:>6.2f}€" if cpl_7d > 0 else " —"
|
||||
lines.append(
|
||||
f"{name:<45} {it['spend']:>5.0f}€ {leads_str} {cpl_3d_str:>8} {cpl_7d_str:>8} {it['ctr']:>4.1f}%"
|
||||
)
|
||||
elif show_bid:
|
||||
cpl_str = f"{it['cpl']:>5.2f}€" if it["leads"] > 0 else " —"
|
||||
cost_cap = it.get("cost_cap_eur")
|
||||
cap_str = f" {cost_cap:>5.2f}€" if cost_cap else " Auto"
|
||||
lines.append(
|
||||
f"{name:<45} {it['spend']:>5.0f}€ {leads_str} {cpl_str} {it['ctr']:>4.1f}%{cap_str}"
|
||||
)
|
||||
else:
|
||||
cap_str = ""
|
||||
lines.append(
|
||||
f"{name:<45} {it['spend']:>5.0f}€ {leads_str} {cpl_str} {it['ctr']:>4.1f}%{cap_str}"
|
||||
)
|
||||
cpl_str = f"{it['cpl']:>5.2f}€" if it["leads"] > 0 else " —"
|
||||
lines.append(
|
||||
f"{name:<45} {it['spend']:>5.0f}€ {leads_str} {cpl_str} {it['ctr']:>4.1f}%"
|
||||
)
|
||||
lines.append("```")
|
||||
# Evaluations below the table
|
||||
for it in items:
|
||||
@ -303,6 +317,8 @@ def send_daily_report(
|
||||
name = detail["name"]
|
||||
vertical = detail["vertical"]
|
||||
margin = detail["margin"]
|
||||
spend_1d = detail.get("spend_1d", 0.0)
|
||||
leads_1d = detail.get("leads_1d", 0)
|
||||
adsets = detail.get("adsets", [])
|
||||
ads = detail.get("ads", [])
|
||||
bid_cfg = detail.get("bid_config", {})
|
||||
@ -319,8 +335,8 @@ def send_daily_report(
|
||||
|
||||
header_text = (
|
||||
f"{emoji} *{name}*\n"
|
||||
f"Vertical: _{vertical}_ | Margen: *{m_str}* | "
|
||||
f"Estrategia: `{strategy_label}` | Presupuesto: {budget_str}\n"
|
||||
f"Vertical: _{vertical}_ | Ayer: *{spend_1d:.0f}€ / {leads_1d} leads* | "
|
||||
f"Margen: *{m_str}* | Estrategia: `{strategy_label}` | Presupuesto: {budget_str}\n"
|
||||
f"*Decisión: {alabel}*"
|
||||
)
|
||||
if action:
|
||||
@ -361,8 +377,8 @@ def send_daily_report(
|
||||
})
|
||||
|
||||
# Tabla adsets + anuncios
|
||||
adset_table = _adset_ad_table(adsets, "Conjuntos de anuncios", show_bid=True)
|
||||
ad_table = _adset_ad_table(ads, "Anuncios")
|
||||
adset_table = _adset_ad_table(adsets, "Conjuntos de anuncios (últimos 3 días)", show_bid=True)
|
||||
ad_table = _adset_ad_table(ads, "Anuncios (últimos 7 días)", show_7d=True)
|
||||
combined = "\n\n".join(filter(None, [adset_table, ad_table]))
|
||||
for chunk in [combined[i:i+2900] for i in range(0, len(combined), 2900)]:
|
||||
camp_blocks.append({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user