Ports meta-optimizer's Meta Ads execution/approval/creative-analysis layer (agent.py, meta_ads_client.py, baserow_client.py, slack_notifier.py, approval_server.py) and replaces the per-vertical CPL model with the PPL + monthly-capping-per-course model already used by leads-optimizer, via a new airtable_client.py that shares Cursos/Familias/CentroCurso/ CursoMes/Leads Lake with that project and adds Meta Ads Campaigns / MetaCampaignMes alongside its Google Ads Campaigns / GACampaignMes.
169 lines
6.8 KiB
Python
169 lines
6.8 KiB
Python
"""Re-send a day's Slack report from Baserow snapshots (sin llamar a Meta por campaña)."""
|
|
import sys
|
|
import io
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", line_buffering=True)
|
|
|
|
import json
|
|
from datetime import datetime
|
|
|
|
import config
|
|
from meta_ads_client import MetaAdsClient
|
|
from airtable_client import AirtableClient, extract_cursoid
|
|
from baserow_client import BaserowClient
|
|
import slack_notifier
|
|
|
|
|
|
def main():
|
|
run_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
|
|
print(f"Reenviando informe para {run_date}...")
|
|
|
|
meta = MetaAdsClient()
|
|
baserow = BaserowClient()
|
|
airtable = AirtableClient()
|
|
|
|
ppl_lookup, _, familia_lookup = airtable.build_campaign_lookups(as_of_date=run_date)
|
|
|
|
# ── Monthly daily totals (fresh de Meta, no se persisten por campaña) ──────
|
|
print("Obteniendo datos mensuales de Meta...")
|
|
month_start = f"{run_date[:7]}-01"
|
|
daily_rows = meta.get_daily_campaign_rows(month_start, run_date)
|
|
_daily: dict = {}
|
|
monthly_familias: dict = {}
|
|
for row in daily_rows:
|
|
cursoid = extract_cursoid(row["campaign_name"]) or ""
|
|
familia = familia_lookup.get(cursoid, "Sin familia")
|
|
ppl = ppl_lookup.get(cursoid, 0)
|
|
margin = round(row["leads"] * ppl - row["spend"], 2)
|
|
d = _daily.setdefault(row["date"], {"spend": 0.0, "leads": 0, "margin": 0.0, "f_margins": {}})
|
|
d["spend"] += row["spend"]
|
|
d["leads"] += row["leads"]
|
|
d["margin"] += margin
|
|
d["f_margins"][familia] = d["f_margins"].get(familia, 0.0) + margin
|
|
mf = monthly_familias.setdefault(familia, {"spend": 0.0, "leads": 0, "margin": 0.0})
|
|
mf["spend"] += row["spend"]
|
|
mf["leads"] += row["leads"]
|
|
mf["margin"] += margin
|
|
daily_totals = [
|
|
{
|
|
"date": date,
|
|
"spend": round(d["spend"], 2),
|
|
"leads": int(d["leads"]),
|
|
"cpl": round(d["spend"] / d["leads"], 2) if d["leads"] > 0 else 0.0,
|
|
"margin": round(d["margin"], 2),
|
|
"f_margins": {f: round(m, 0) for f, m in d["f_margins"].items()},
|
|
}
|
|
for date, d in sorted(_daily.items())
|
|
]
|
|
print(f" ✓ {len(daily_totals)} días con datos")
|
|
|
|
# ── Load proposed actions (to get parameter values) ──────────────────────
|
|
action_params: dict = {} # campaign_name → parameter
|
|
try:
|
|
all_actions = baserow._get_rows(config.BASEROW_TABLE_ACTIONS, {
|
|
"filter__proposed_at__equal": run_date,
|
|
})
|
|
for a in all_actions:
|
|
cname = a.get("campaign_name", "")
|
|
param = a.get("parameter")
|
|
if cname and param:
|
|
action_params[cname] = float(param)
|
|
print(f" ✓ {len(action_params)} parámetros de acción cargados")
|
|
except Exception as e:
|
|
print(f" Aviso: no se pudieron cargar parámetros de acción: {e}")
|
|
|
|
# ── Load snapshots from Baserow ───────────────────────────────────────────
|
|
print(f"Cargando snapshots de Baserow para {run_date}...")
|
|
snapshots = baserow.get_snapshots_for_date(run_date)
|
|
print(f" ✓ {len(snapshots)} snapshots encontrados")
|
|
|
|
if not snapshots:
|
|
print("ERROR: No hay snapshots en Baserow para esta fecha. Ejecuta run.py primero.")
|
|
return
|
|
|
|
# ── Reconstruct data structures ───────────────────────────────────────────
|
|
# Nota: urgencia/leads_mes/capping no se persisten en daily_snapshots, así
|
|
# que al reenviar desde snapshots esos campos salen con su valor por
|
|
# defecto (slack_notifier ya los trata con .get(...)).
|
|
campaign_details: dict = {}
|
|
actions: list = []
|
|
familias: dict = {}
|
|
metrics_all: dict = {}
|
|
|
|
for snap in snapshots:
|
|
cid = snap.get("campaign_id") or snap.get("campaign_name", "")
|
|
name = snap["campaign_name"]
|
|
familia = snap.get("familia") or familia_lookup.get(extract_cursoid(name) or "", "Sin familia")
|
|
margin = float(snap.get("margin") or 0)
|
|
spend = float(snap.get("spend") or 0)
|
|
leads = int(snap.get("leads") or 0)
|
|
cpl = float(snap.get("cpl") or 0)
|
|
action_type = snap.get("action_type") or "MAINTAIN"
|
|
|
|
try:
|
|
adsets = json.loads(snap.get("adsets_json") or "[]")
|
|
except Exception:
|
|
adsets = []
|
|
try:
|
|
ads = json.loads(snap.get("ads_json") or "[]")
|
|
except Exception:
|
|
ads = []
|
|
|
|
campaign_details[cid] = {
|
|
"name": name,
|
|
"familia": familia,
|
|
"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}
|
|
|
|
if action_type != "MAINTAIN":
|
|
actions.append({
|
|
"campaign_name": name,
|
|
"action_type": action_type,
|
|
"justification": snap.get("justification") or "",
|
|
"advice": "",
|
|
"alert": "",
|
|
"confidence": 0.8,
|
|
"parameter": action_params.get(name, 1.0),
|
|
"row_id": snap["id"],
|
|
})
|
|
|
|
f = familias.setdefault(familia, {"spend": 0.0, "leads": 0, "margin": 0.0})
|
|
f["spend"] += spend
|
|
f["leads"] += leads
|
|
f["margin"] += margin
|
|
|
|
# ── Best / worst ──────────────────────────────────────────────────────────
|
|
with_leads = [m for m in metrics_all.values() if m["leads"] > 0]
|
|
best_10 = sorted(with_leads, key=lambda x: x["cpl"])[:10]
|
|
worst_10 = sorted(
|
|
list(metrics_all.values()),
|
|
key=lambda x: (x["leads"] > 0, -x["cpl"] if x["leads"] > 0 else 0),
|
|
)[:10]
|
|
|
|
# ── Send ──────────────────────────────────────────────────────────────────
|
|
print("Enviando a Slack...")
|
|
ts = slack_notifier.send_daily_report(
|
|
daily_totals=daily_totals,
|
|
best_campaigns=best_10,
|
|
worst_campaigns=worst_10,
|
|
actions=actions,
|
|
campaigns_analyzed=len(snapshots),
|
|
mode="DRY_RUN",
|
|
familias=familias,
|
|
campaign_details=campaign_details,
|
|
monthly_familias=monthly_familias,
|
|
)
|
|
if ts:
|
|
print(f" ✓ Mensaje enviado (ts={ts})")
|
|
else:
|
|
print(" ✗ Error al enviar (revisa token y canal)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|