Fix lead counting, call tracking, pause thresholds, and ABO budget detection
- _count_conversions: prioritize 'lead' over 'lead_grouped' to fix double-counting (optimizer was showing 2x actual leads on all campaigns) - Add call_confirm/contact fallbacks for call-objective campaigns (Vodafone, Lowi) - PAUSE at campaign level only when leads==0 AND spend >= 3x max_cpl (technical failure) - Ad-level PAUSE threshold raised from 5€ fixed to 3x max_cpl (context-aware) - Pass max_cpl to ad analysis so Claude uses the correct campaign target - Skip INCREASE/REDUCE_BUDGET for ABO campaigns (no campaign-level daily budget) - Fetch bid config before action save to enable ABO detection pre-Baserow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e5c0c9f6a6
commit
8b6ec106ee
8
agent.py
8
agent.py
@ -12,11 +12,11 @@ Cada campaña tiene un CPL máximo (coste por lead objetivo) que define el lími
|
||||
USA SIEMPRE € como unidad de moneda. Responde SIEMPRE en español.
|
||||
|
||||
REGLAS DE DECISIÓN:
|
||||
1. CPL > max_cpl → REDUCE_BUDGET o revisar creatividades/audiencias.
|
||||
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. Sin leads tras varios días de inversión → revisar configuración de conversiones.
|
||||
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.
|
||||
|
||||
Responde SOLO con JSON válido, sin texto adicional ni markdown:
|
||||
{
|
||||
@ -79,8 +79,8 @@ Eres un analista experto en Meta Ads. Analiza las métricas del anuncio indicado
|
||||
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 anuncio ha gastado más de 5€ con 0 leads, o su CPL es más del doble del objetivo sin signos de mejora.
|
||||
En caso contrario usa "accion": "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.
|
||||
En cualquier otro caso usa "accion": "MAINTAIN".
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@ -22,14 +22,23 @@ class MetaAdsClient:
|
||||
)
|
||||
self.account = AdAccount(config.META_AD_ACCOUNT_ID)
|
||||
|
||||
@staticmethod
|
||||
def _count_conversions(actions: list) -> float:
|
||||
"""Prioritize 'lead' to avoid double-counting with lead_grouped; include call_confirm."""
|
||||
by_type = {a["action_type"]: float(a["value"]) for a in actions}
|
||||
if "lead" in by_type:
|
||||
return by_type["lead"]
|
||||
if "onsite_conversion.lead_grouped" in by_type:
|
||||
return by_type["onsite_conversion.lead_grouped"]
|
||||
return by_type.get("call_confirm", by_type.get("contact", 0.0))
|
||||
|
||||
def _parse_insights_row(self, row: dict) -> dict:
|
||||
spend = float(row.get("spend", 0))
|
||||
impressions = int(row.get("impressions", 0))
|
||||
clicks = int(row.get("clicks", 0))
|
||||
ctr = float(row.get("ctr", 0))
|
||||
cpm = float(row.get("cpm", 0))
|
||||
leads = sum(float(a["value"]) for a in row.get("actions", [])
|
||||
if a["action_type"] in ("lead", "onsite_conversion.lead_grouped"))
|
||||
leads = self._count_conversions(row.get("actions", []))
|
||||
cpl = round(spend / leads, 2) if leads > 0 else 0.0
|
||||
return {
|
||||
"campaign_id": row.get("campaign_id", ""),
|
||||
@ -89,8 +98,7 @@ class MetaAdsClient:
|
||||
if not row.get("campaign_name", "").upper().startswith(prefix):
|
||||
continue
|
||||
spend = float(row.get("spend", 0))
|
||||
leads = sum(float(a["value"]) for a in row.get("actions", [])
|
||||
if a["action_type"] in ("lead", "onsite_conversion.lead_grouped"))
|
||||
leads = self._count_conversions(row.get("actions", []))
|
||||
result.append({
|
||||
"date": row["date_start"],
|
||||
"campaign_name": row.get("campaign_name", ""),
|
||||
@ -120,8 +128,7 @@ class MetaAdsClient:
|
||||
spend = float(row.get("spend", 0))
|
||||
if spend == 0:
|
||||
continue
|
||||
leads = sum(float(a["value"]) for a in row.get("actions", [])
|
||||
if a["action_type"] in ("lead", "onsite_conversion.lead_grouped"))
|
||||
leads = self._count_conversions(row.get("actions", []))
|
||||
cpl = round(spend / leads, 2) if leads > 0 else 0.0
|
||||
result.append({
|
||||
"id": row.get(id_field, ""),
|
||||
|
||||
33
run.py
33
run.py
@ -181,6 +181,24 @@ def run():
|
||||
"MAINTAIN",
|
||||
)
|
||||
|
||||
# ── Bid config (campaña + adsets) ─────────────────────────────────
|
||||
campaign_bid = {}
|
||||
try:
|
||||
campaign_bid = meta.get_campaign_bid_config(cid)
|
||||
except Exception as e:
|
||||
errors.append(f"Bid config {metrics['name']}: {e}")
|
||||
|
||||
adset_bids = {}
|
||||
try:
|
||||
adset_bids = meta.get_adset_bid_configs(cid)
|
||||
except Exception as e:
|
||||
errors.append(f"Adset bids {metrics['name']}: {e}")
|
||||
|
||||
# ABO campaigns (presupuesto solo a nivel de conjunto): omitir ajustes de campaña
|
||||
is_cbo = campaign_bid.get("daily_budget_eur") is not None
|
||||
if action_type in ("INCREASE_BUDGET", "REDUCE_BUDGET") and not is_cbo:
|
||||
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" Vertical: {vertical} Decision: {action_type} — {(decision.get('justification') or '')[:70]}")
|
||||
@ -216,20 +234,6 @@ def run():
|
||||
errors.append(f"Save action {metrics['name']}: {e}")
|
||||
|
||||
# ── Ad set analysis ────────────────────────────────────────────────
|
||||
# ── Bid config (campaña + adsets) — antes del análisis para pasarlo a Claude ──
|
||||
campaign_bid = {}
|
||||
try:
|
||||
campaign_bid = meta.get_campaign_bid_config(cid)
|
||||
except Exception as e:
|
||||
errors.append(f"Bid config {metrics['name']}: {e}")
|
||||
|
||||
adset_bids = {}
|
||||
try:
|
||||
adset_bids = meta.get_adset_bid_configs(cid)
|
||||
except Exception as e:
|
||||
errors.append(f"Adset bids {metrics['name']}: {e}")
|
||||
|
||||
# ── Ad set analysis (con cost_cap_eur disponible para Claude) ──────────
|
||||
adsets_detail = []
|
||||
try:
|
||||
for as_m in meta.get_yesterday_adset_metrics(cid)[:5]:
|
||||
@ -246,6 +250,7 @@ def run():
|
||||
ads_detail = []
|
||||
try:
|
||||
for ad_m in meta.get_yesterday_ad_metrics(cid)[:5]:
|
||||
ad_m["max_cpl"] = max_cpl
|
||||
result = analyze_unit(ad_m, "ad")
|
||||
ad_entry = {**ad_m, **result}
|
||||
# Propose ad pause if Claude recommends it
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user