Fix creative-analysis JSON truncation bug; add Meta/Airtable split to dashboard
- analyze_creative_deep was truncating mid-JSON ~50-75% of the time (max_tokens=700 too low once the analysis text runs long), silently falling back to score=0/"Error parseando respuesta." Confirmed via stop_reason=max_tokens on real API calls. Raised to 1400, and bumped analyze_creative/compare_adset_creatives (600→900) as the same risk applies there. Verified 6/6 clean parses after the fix (was failing ~3/4 before). - Dashboard tabs "Campañas", "Familias", and the per-day breakdown in "Por día" still showed a single Meta-only Leads/CPL number; added the Leads/CPL según Airtable (leadform+landing) columns alongside, matching the Slack report so the dashboard never shows a different figure than what's already trusted from the daily report. - Validated both end-to-end with real data: dashboard via Streamlit's AppTest headless runner (0 exceptions across all 5 tabs), creative analyzer via a live run against one campaign (real scores, fatigue detection, comparison, Baserow save, Slack send all confirmed).
This commit is contained in:
parent
921000c47e
commit
0016809ece
6
agent.py
6
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}],
|
||||
)
|
||||
|
||||
102
dashboard.py
102
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
|
||||
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 "—",
|
||||
"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,14 +352,19 @@ 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)
|
||||
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 "—",
|
||||
"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}%",
|
||||
@ -332,6 +373,8 @@ with tab2:
|
||||
|
||||
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,31 +441,50 @@ 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
|
||||
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),
|
||||
"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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user