diff --git a/agent.py b/agent.py index bfee12c..564db94 100644 --- a/agent.py +++ b/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. USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español. -REGLAS DE DECISIÓN: -1. CPL > max_cpl → REDUCE_BUDGET (nunca PAUSE por CPL alto). -2. CPL <= max_cpl con bajo volumen → INCREASE_BUDGET si hay margen. -3. Frecuencia > 3.0 → considera rotar creatividades o ampliar audiencia. -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. +Recibirás métricas en tres ventanas temporales: +- spend/leads/cpl: ayer (1 día, contexto inmediato) +- spend_3d/leads_3d/cpl_3d: últimos 3 días (ventana PRINCIPAL para decisiones) +- spend_7d/leads_7d/cpl_7d: últimos 7 días (tendencia de fondo) + +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: { @@ -67,6 +74,7 @@ def decide(analysis: dict) -> dict: UNIT_SYSTEM = """ 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. 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). @@ -76,6 +84,7 @@ 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). 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"} diff --git a/meta_ads_client.py b/meta_ads_client.py index c317856..e0243cb 100644 --- a/meta_ads_client.py +++ b/meta_ads_client.py @@ -168,6 +168,24 @@ class MetaAdsClient: yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") 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: """ Returns active ads for a campaign with their thumbnail URLs for creative analysis. diff --git a/run.py b/run.py index 9ac50fb..9412c95 100644 --- a/run.py +++ b/run.py @@ -145,6 +145,12 @@ def run(): metrics_all = meta.get_yesterday_metrics() 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 ─────────────────────────────────── actions_proposed_list = [] 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 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 = { "campaign_id": cid, "name": metrics["name"], "status": metrics["status"], + # Yesterday (1d) — contexto inmediato "spend": metrics["spend"], "leads": metrics["leads"], "cpl": metrics["cpl"], @@ -168,6 +177,14 @@ def run(): "cpm": metrics["cpm"], "impressions": metrics["impressions"], "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: @@ -200,7 +217,8 @@ def run(): action_type = "MAINTAIN" 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]}") if decision.get("alert"): print(f" ALERT: {decision['alert']}") @@ -236,7 +254,7 @@ def run(): # ── Ad set analysis ──────────────────────────────────────────────── adsets_detail = [] 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"], {}) as_m["bid_strategy"] = bid.get("bid_strategy", "") as_m["cost_cap_eur"] = bid.get("cost_cap_eur") @@ -249,7 +267,7 @@ def run(): # ── Ad analysis ──────────────────────────────────────────────────── ads_detail = [] 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 result = analyze_unit(ad_m, "ad") ad_entry = {**ad_m, **result}