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:
|
try:
|
||||||
response = client.messages.create(
|
response = client.messages.create(
|
||||||
model="claude-sonnet-4-6",
|
model="claude-sonnet-4-6",
|
||||||
max_tokens=600,
|
max_tokens=900,
|
||||||
system=CREATIVE_SYSTEM,
|
system=CREATIVE_SYSTEM,
|
||||||
messages=[{
|
messages=[{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@ -429,7 +429,7 @@ def analyze_creative_deep(image_url: str, ad_name: str, metrics: dict) -> dict:
|
|||||||
try:
|
try:
|
||||||
response = client.messages.create(
|
response = client.messages.create(
|
||||||
model="claude-sonnet-4-6",
|
model="claude-sonnet-4-6",
|
||||||
max_tokens=700,
|
max_tokens=1400,
|
||||||
system=CREATIVE_DEEP_SYSTEM,
|
system=CREATIVE_DEEP_SYSTEM,
|
||||||
messages=[{
|
messages=[{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@ -484,7 +484,7 @@ def compare_adset_creatives(ads: list) -> dict:
|
|||||||
try:
|
try:
|
||||||
response = client.messages.create(
|
response = client.messages.create(
|
||||||
model="claude-sonnet-4-6",
|
model="claude-sonnet-4-6",
|
||||||
max_tokens=600,
|
max_tokens=900,
|
||||||
system=CREATIVE_COMPARE_SYSTEM,
|
system=CREATIVE_COMPARE_SYSTEM,
|
||||||
messages=[{"role": "user", "content": content_blocks}],
|
messages=[{"role": "user", "content": content_blocks}],
|
||||||
)
|
)
|
||||||
|
|||||||
136
dashboard.py
136
dashboard.py
@ -96,6 +96,27 @@ def _load_data(date_from: str, date_to: str):
|
|||||||
return daily_rows, campaign_metrics
|
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)...")
|
@st.cache_data(ttl=300, show_spinner="Cargando métricas diarias (Baserow)...")
|
||||||
def _load_daily_metrics(date_from: str, date_to: str):
|
def _load_daily_metrics(date_from: str, date_to: str):
|
||||||
"""Totales diarios persistidos por run.py (Leads Meta vs Leads Airtable) —
|
"""Totales diarios persistidos por run.py (Leads Meta vs Leads Airtable) —
|
||||||
@ -258,28 +279,38 @@ with tab1:
|
|||||||
key="t1_day",
|
key="t1_day",
|
||||||
)
|
)
|
||||||
if selected_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 = {}
|
day_camp: dict = {}
|
||||||
for row in daily_rows:
|
for row in daily_rows:
|
||||||
if row["date"] != selected_day:
|
if row["date"] != selected_day:
|
||||||
continue
|
continue
|
||||||
k = row["campaign_name"]
|
k = row["campaign_name"]
|
||||||
if k not in day_camp:
|
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),
|
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]["spend"] += row["spend"]
|
||||||
day_camp[k]["leads"] += row["leads"]
|
day_camp[k]["leads"] += row["leads"]
|
||||||
|
|
||||||
camp_rows = []
|
camp_rows = []
|
||||||
for c in sorted(day_camp.values(), key=lambda x: -x["spend"]):
|
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
|
cpl = round(c["spend"] / c["leads"], 2) if c["leads"] > 0 else 0.0
|
||||||
margin = round(c["leads"] * c["ppl"] - c["spend"], 2)
|
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({
|
camp_rows.append({
|
||||||
"Campaña": c["name"],
|
"Campaña": c["name"],
|
||||||
"Familia": c["familia"],
|
"Familia": c["familia"],
|
||||||
"Gasto": _eur(c["spend"]),
|
"Gasto": _eur(c["spend"]),
|
||||||
"Leads": c["leads"],
|
"Leads Meta": c["leads"],
|
||||||
"CPL": _eur(cpl) if c["leads"] > 0 else "—",
|
"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 "—",
|
"PPL": _eur(c["ppl"]) if c["ppl"] else "—",
|
||||||
"Margen": _margin(margin),
|
"Margen": _margin(margin),
|
||||||
})
|
})
|
||||||
@ -301,6 +332,11 @@ with tab2:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"Error cargando datos de Meta API: {e}")
|
st.error(f"Error cargando datos de Meta API: {e}")
|
||||||
campaign_metrics_2 = {}
|
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()})
|
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")
|
sel_fam_2 = col_fam_2.selectbox("Familia", fam_opts_2, key="t2_fam")
|
||||||
@ -316,22 +352,29 @@ with tab2:
|
|||||||
else:
|
else:
|
||||||
camp_rows = []
|
camp_rows = []
|
||||||
for cid, m in sorted(campaign_metrics_2.items(), key=lambda x: -x[1]["spend"]):
|
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 ""
|
||||||
margin = round(m["leads"] * ppl - m["spend"], 2)
|
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({
|
camp_rows.append({
|
||||||
"Campaña": m["name"],
|
"Campaña": m["name"],
|
||||||
"Familia": _familia_of(m["name"], familia_lookup),
|
"Familia": _familia_of(m["name"], familia_lookup),
|
||||||
"Gasto": _eur(m["spend"]),
|
"Gasto": _eur(m["spend"]),
|
||||||
"Leads": m["leads"],
|
"Leads Meta": m["leads"],
|
||||||
"CPL": _eur(m["cpl"]) if m["leads"] > 0 else "—",
|
"Leads AT": leads_at,
|
||||||
"PPL": _eur(ppl) if ppl else "—",
|
"CPL Meta": _eur(m["cpl"]) if m["leads"] > 0 else "—",
|
||||||
"Margen": _margin(margin),
|
"CPL AT": _eur(cpl_at) if leads_at > 0 else "—",
|
||||||
"CTR": f"{m['ctr']:.1f}%",
|
"PPL": _eur(ppl) if ppl else "—",
|
||||||
"_cid": cid,
|
"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])
|
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.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")
|
st.subheader("Detalle de campaña")
|
||||||
camp_id_map = {r["Campaña"]: r["_cid"] for r in camp_rows}
|
camp_id_map = {r["Campaña"]: r["_cid"] for r in camp_rows}
|
||||||
@ -398,32 +441,51 @@ with tab3:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"Error cargando datos de Meta API: {e}")
|
st.error(f"Error cargando datos de Meta API: {e}")
|
||||||
campaign_metrics_3 = {}
|
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 = {}
|
familias_3: dict = {}
|
||||||
for cid, m in campaign_metrics_3.items():
|
for cursoid, ca in curso_agg.items():
|
||||||
fam = _familia_of(m["name"], familia_lookup)
|
leads_at = at_leads_3.get(cursoid, 0)
|
||||||
ppl = ppl_lookup.get(extract_cursoid(m["name"]) or "", 0)
|
margin = ca["leads_meta"] * ca["ppl"] - ca["spend"]
|
||||||
margin = round(m["leads"] * ppl - m["spend"], 2)
|
f = familias_3.setdefault(ca["familia"], {"spend": 0.0, "leads_meta": 0, "leads_at": 0, "margin": 0.0})
|
||||||
if fam not in familias_3:
|
f["spend"] += ca["spend"]
|
||||||
familias_3[fam] = {"spend": 0.0, "leads": 0, "margin": 0.0}
|
f["leads_meta"] += ca["leads_meta"]
|
||||||
familias_3[fam]["spend"] += m["spend"]
|
f["leads_at"] += leads_at
|
||||||
familias_3[fam]["leads"] += m["leads"]
|
f["margin"] += margin
|
||||||
familias_3[fam]["margin"] += margin
|
|
||||||
|
|
||||||
if not familias_3:
|
if not familias_3:
|
||||||
st.info("Sin datos de familias.")
|
st.info("Sin datos de familias.")
|
||||||
else:
|
else:
|
||||||
fam_rows = []
|
fam_rows = []
|
||||||
for fam, data in sorted(familias_3.items(), key=lambda x: -x[1]["margin"]):
|
for fam, data in sorted(familias_3.items(), key=lambda x: -x[1]["margin"]):
|
||||||
f_leads = data["leads"]
|
f_spend = data["spend"]
|
||||||
f_spend = data["spend"]
|
cpl_meta = round(f_spend / data["leads_meta"], 2) if data["leads_meta"] > 0 else 0.0
|
||||||
f_cpl = round(f_spend / f_leads, 2) if f_leads > 0 else 0.0
|
cpl_at = round(f_spend / data["leads_at"], 2) if data["leads_at"] > 0 else 0.0
|
||||||
fam_rows.append({
|
fam_rows.append({
|
||||||
"Familia": fam,
|
"Familia": fam,
|
||||||
"Gasto": _eur(f_spend),
|
"Gasto": _eur(f_spend),
|
||||||
"Leads": f_leads,
|
"Leads Meta": data["leads_meta"],
|
||||||
"CPL": _eur(f_cpl),
|
"Leads AT": data["leads_at"],
|
||||||
"Margen": _margin(data["margin"]),
|
"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)
|
st.dataframe(pd.DataFrame(fam_rows), use_container_width=True, hide_index=True)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user