diff --git a/agent.py b/agent.py index a70dad8..9994666 100644 --- a/agent.py +++ b/agent.py @@ -287,7 +287,7 @@ def analyze_creative(image_url: str, ad_name: str) -> dict: try: response = client.messages.create( model="claude-sonnet-4-6", - max_tokens=600, + max_tokens=900, system=CREATIVE_SYSTEM, messages=[{ "role": "user", @@ -429,7 +429,7 @@ def analyze_creative_deep(image_url: str, ad_name: str, metrics: dict) -> dict: try: response = client.messages.create( model="claude-sonnet-4-6", - max_tokens=700, + max_tokens=1400, system=CREATIVE_DEEP_SYSTEM, messages=[{ "role": "user", @@ -484,7 +484,7 @@ def compare_adset_creatives(ads: list) -> dict: try: response = client.messages.create( model="claude-sonnet-4-6", - max_tokens=600, + max_tokens=900, system=CREATIVE_COMPARE_SYSTEM, messages=[{"role": "user", "content": content_blocks}], ) diff --git a/dashboard.py b/dashboard.py index 3e9b01a..b242dad 100644 --- a/dashboard.py +++ b/dashboard.py @@ -96,6 +96,27 @@ def _load_data(date_from: str, date_to: str): return daily_rows, campaign_metrics +@st.cache_data(ttl=300, show_spinner="Cargando leads de Airtable...") +def _load_at_leads_by_cursoid(date_from: str, date_to: str) -> dict: + """{cursoid: nº leads en Leads Lake (leadform+landing) en el rango}.""" + leads = AirtableClient().get_meta_leads_bulk(date_from, date_to) + counts: dict = {} + for l in leads: + counts[l["cursoid"]] = counts.get(l["cursoid"], 0) + 1 + return counts + + +@st.cache_data(ttl=300, show_spinner="Cargando leads de Airtable por día...") +def _load_at_leads_by_day_cursoid(date_from: str, date_to: str) -> dict: + """{(date, cursoid): nº leads en Leads Lake (leadform+landing)}.""" + leads = AirtableClient().get_meta_leads_bulk(date_from, date_to) + counts: dict = {} + for l in leads: + key = (l["date"], l["cursoid"]) + counts[key] = counts.get(key, 0) + 1 + return counts + + @st.cache_data(ttl=300, show_spinner="Cargando métricas diarias (Baserow)...") def _load_daily_metrics(date_from: str, date_to: str): """Totales diarios persistidos por run.py (Leads Meta vs Leads Airtable) — @@ -258,28 +279,38 @@ with tab1: key="t1_day", ) if selected_day: + try: + at_leads_day = _load_at_leads_by_day_cursoid(day_opts[-1], day_opts[0]) + except Exception: + at_leads_day = {} + day_camp: dict = {} for row in daily_rows: if row["date"] != selected_day: continue k = row["campaign_name"] if k not in day_camp: - ppl = ppl_lookup.get(extract_cursoid(k) or "", 0) + cursoid = extract_cursoid(k) or "" + ppl = ppl_lookup.get(cursoid, 0) day_camp[k] = {"name": k, "familia": _familia_of(k, familia_lookup), - "spend": 0.0, "leads": 0, "ppl": ppl} + "spend": 0.0, "leads": 0, "ppl": ppl, "cursoid": cursoid} day_camp[k]["spend"] += row["spend"] day_camp[k]["leads"] += row["leads"] camp_rows = [] for c in sorted(day_camp.values(), key=lambda x: -x["spend"]): - cpl = round(c["spend"] / c["leads"], 2) if c["leads"] > 0 else 0.0 - margin = round(c["leads"] * c["ppl"] - c["spend"], 2) + cpl = round(c["spend"] / c["leads"], 2) if c["leads"] > 0 else 0.0 + leads_at = at_leads_day.get((selected_day, c["cursoid"]), 0) + cpl_at = round(c["spend"] / leads_at, 2) if leads_at > 0 else 0.0 + margin = round(c["leads"] * c["ppl"] - c["spend"], 2) camp_rows.append({ - "Campaña": c["name"], - "Familia": c["familia"], - "Gasto": _eur(c["spend"]), - "Leads": c["leads"], - "CPL": _eur(cpl) if c["leads"] > 0 else "—", + "Campaña": c["name"], + "Familia": c["familia"], + "Gasto": _eur(c["spend"]), + "Leads Meta": c["leads"], + "Leads AT": leads_at, + "CPL Meta": _eur(cpl) if c["leads"] > 0 else "—", + "CPL AT": _eur(cpl_at) if leads_at > 0 else "—", "PPL": _eur(c["ppl"]) if c["ppl"] else "—", "Margen": _margin(margin), }) @@ -301,6 +332,11 @@ with tab2: except Exception as e: st.error(f"Error cargando datos de Meta API: {e}") campaign_metrics_2 = {} + try: + at_leads_2 = _load_at_leads_by_cursoid(d_from_2.strftime("%Y-%m-%d"), d_to_2.strftime("%Y-%m-%d")) + except Exception as e: + st.error(f"Error cargando leads de Airtable: {e}") + at_leads_2 = {} fam_opts_2 = ["Todas"] + sorted({_familia_of(m["name"], familia_lookup) for m in campaign_metrics_2.values()}) sel_fam_2 = col_fam_2.selectbox("Familia", fam_opts_2, key="t2_fam") @@ -316,22 +352,29 @@ with tab2: else: camp_rows = [] for cid, m in sorted(campaign_metrics_2.items(), key=lambda x: -x[1]["spend"]): - ppl = ppl_lookup.get(extract_cursoid(m["name"]) or "", 0) - margin = round(m["leads"] * ppl - m["spend"], 2) + cursoid = extract_cursoid(m["name"]) or "" + ppl = ppl_lookup.get(cursoid, 0) + leads_at = at_leads_2.get(cursoid, 0) + cpl_at = round(m["spend"] / leads_at, 2) if leads_at > 0 else 0.0 + margin = round(m["leads"] * ppl - m["spend"], 2) camp_rows.append({ - "Campaña": m["name"], - "Familia": _familia_of(m["name"], familia_lookup), - "Gasto": _eur(m["spend"]), - "Leads": m["leads"], - "CPL": _eur(m["cpl"]) if m["leads"] > 0 else "—", - "PPL": _eur(ppl) if ppl else "—", - "Margen": _margin(margin), - "CTR": f"{m['ctr']:.1f}%", - "_cid": cid, + "Campaña": m["name"], + "Familia": _familia_of(m["name"], familia_lookup), + "Gasto": _eur(m["spend"]), + "Leads Meta": m["leads"], + "Leads AT": leads_at, + "CPL Meta": _eur(m["cpl"]) if m["leads"] > 0 else "—", + "CPL AT": _eur(cpl_at) if leads_at > 0 else "—", + "PPL": _eur(ppl) if ppl else "—", + "Margen": _margin(margin), + "CTR": f"{m['ctr']:.1f}%", + "_cid": cid, }) df_camps = pd.DataFrame([{k: v for k, v in r.items() if k != "_cid"} for r in camp_rows]) st.dataframe(df_camps, use_container_width=True, hide_index=True) + st.caption("Leads AT = leads en Leads Lake (leadform + landing) del curso completo — si el curso " + "tiene más de una campaña activa, el mismo total aparece en cada una.") st.subheader("Detalle de campaña") camp_id_map = {r["Campaña"]: r["_cid"] for r in camp_rows} @@ -398,32 +441,51 @@ with tab3: except Exception as e: st.error(f"Error cargando datos de Meta API: {e}") campaign_metrics_3 = {} + try: + at_leads_3 = _load_at_leads_by_cursoid(d_from_3.strftime("%Y-%m-%d"), d_to_3.strftime("%Y-%m-%d")) + except Exception as e: + st.error(f"Error cargando leads de Airtable: {e}") + at_leads_3 = {} + + # Agregar primero por curso (único), luego por familia — si un curso + # tiene 2 campañas activas no se duplican sus leads de Airtable. + curso_agg: dict = {} + for cid, m in campaign_metrics_3.items(): + cursoid = extract_cursoid(m["name"]) or "" + ca = curso_agg.setdefault(cursoid, { + "spend": 0.0, "leads_meta": 0, + "familia": familia_lookup.get(cursoid, "Sin familia"), + "ppl": ppl_lookup.get(cursoid, 0), + }) + ca["spend"] += m["spend"] + ca["leads_meta"] += m["leads"] familias_3: dict = {} - for cid, m in campaign_metrics_3.items(): - fam = _familia_of(m["name"], familia_lookup) - ppl = ppl_lookup.get(extract_cursoid(m["name"]) or "", 0) - margin = round(m["leads"] * ppl - m["spend"], 2) - if fam not in familias_3: - familias_3[fam] = {"spend": 0.0, "leads": 0, "margin": 0.0} - familias_3[fam]["spend"] += m["spend"] - familias_3[fam]["leads"] += m["leads"] - familias_3[fam]["margin"] += margin + for cursoid, ca in curso_agg.items(): + leads_at = at_leads_3.get(cursoid, 0) + margin = ca["leads_meta"] * ca["ppl"] - ca["spend"] + f = familias_3.setdefault(ca["familia"], {"spend": 0.0, "leads_meta": 0, "leads_at": 0, "margin": 0.0}) + f["spend"] += ca["spend"] + f["leads_meta"] += ca["leads_meta"] + f["leads_at"] += leads_at + f["margin"] += margin if not familias_3: st.info("Sin datos de familias.") else: fam_rows = [] for fam, data in sorted(familias_3.items(), key=lambda x: -x[1]["margin"]): - f_leads = data["leads"] - f_spend = data["spend"] - f_cpl = round(f_spend / f_leads, 2) if f_leads > 0 else 0.0 + f_spend = data["spend"] + cpl_meta = round(f_spend / data["leads_meta"], 2) if data["leads_meta"] > 0 else 0.0 + cpl_at = round(f_spend / data["leads_at"], 2) if data["leads_at"] > 0 else 0.0 fam_rows.append({ - "Familia": fam, - "Gasto": _eur(f_spend), - "Leads": f_leads, - "CPL": _eur(f_cpl), - "Margen": _margin(data["margin"]), + "Familia": fam, + "Gasto": _eur(f_spend), + "Leads Meta": data["leads_meta"], + "Leads AT": data["leads_at"], + "CPL Meta": _eur(cpl_meta) if data["leads_meta"] > 0 else "—", + "CPL AT": _eur(cpl_at) if data["leads_at"] > 0 else "—", + "Margen": _margin(data["margin"]), }) st.dataframe(pd.DataFrame(fam_rows), use_container_width=True, hide_index=True)