Fix leadform double-attribution and Slack message truncation

- airtable_client: restrict leadform cursoid path (path 5) to PMX-only
  campaigns, preventing triple-attribution to search+pmx+leadform companion
- slack_reporter: derive margen from ingreso-coste instead of stale MetricasDiarias
  field; split monthly rankings from fields to separate section blocks; add
  _trunc() helper to clamp all text blocks under Slack's 3000-char limit
- agent: increase portfolio_daily_analysis max_tokens 500→800
- backfill: extend to June 11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Manuel 2026-06-11 23:04:40 +02:00
parent 0eb360bf6d
commit 4848253c49
4 changed files with 36 additions and 24 deletions

View File

@ -141,7 +141,7 @@ def portfolio_daily_analysis(collected: list) -> str:
try: try:
response = client.messages.create( response = client.messages.create(
model="claude-sonnet-4-6", model="claude-sonnet-4-6",
max_tokens=500, max_tokens=800,
system=PORTFOLIO_SYSTEM, system=PORTFOLIO_SYSTEM,
messages=[{ messages=[{
"role": "user", "role": "user",

View File

@ -359,7 +359,10 @@ class AirtableClient:
m = re.search(r'fco_(?:search|pmx)_(\d+)', campaign_name, re.IGNORECASE) m = re.search(r'fco_(?:search|pmx)_(\d+)', campaign_name, re.IGNORECASE)
if m: if m:
course_num = m.group(1) course_num = m.group(1)
if "pmx" in campaign_name.lower() and "_leadform" not in campaign_name.lower(): is_pmx_non_leadform = (
"pmx" in campaign_name.lower() and "_leadform" not in campaign_name.lower()
)
if is_pmx_non_leadform:
utm_source = "pmx" utm_source = "pmx"
elif "search" in campaign_name.lower(): elif "search" in campaign_name.lower():
utm_source = "google" utm_source = "google"
@ -370,10 +373,12 @@ class AirtableClient:
f"AND({{attr_cursoid}}='{course_num}'," f"AND({{attr_cursoid}}='{course_num}',"
f"{{attr_utm_source}}='{utm_source}')" f"{{attr_utm_source}}='{utm_source}')"
) )
leadform_cursoid_clause = ( # Path 5: leadform leads en Airtable, solo para PMX (no search, no _leadform)
f"AND({{attr_cursoid}}='{course_num}'," if is_pmx_non_leadform:
f"{{UserAgent del visitante}}='Google-Ads-Notifications')" leadform_cursoid_clause = (
) f"AND({{attr_cursoid}}='{course_num}',"
f"{{UserAgent del visitante}}='Google-Ads-Notifications')"
)
formula = ( formula = (
f"AND(" f"AND("

View File

@ -8,7 +8,7 @@ import re
from datetime import datetime from datetime import datetime
from airtable_client import AirtableClient from airtable_client import AirtableClient
DAYS = ["2026-06-08", "2026-06-09", "2026-06-10"] DAYS = ["2026-06-08", "2026-06-09", "2026-06-10", "2026-06-11"]
def _course_num(name: str) -> str | None: def _course_num(name: str) -> str | None:

View File

@ -46,14 +46,20 @@ def _last_n_days_combined(current_md: dict, prev_md: dict, now: datetime, n: int
entries.sort(key=lambda x: x[0]) entries.sort(key=lambda x: x[0])
last_n = entries[-n:] last_n = entries[-n:]
total_coste = round(sum(v.get("coste", 0) for _, v in last_n), 2)
total_ingreso = round(sum(v.get("ingreso", 0) for _, v in last_n), 2)
return { return {
"coste": round(sum(v.get("coste", 0) for _, v in last_n), 2), "coste": total_coste,
"ingreso": round(sum(v.get("ingreso", 0) for _, v in last_n), 2), "ingreso": total_ingreso,
"margen": round(sum(v.get("margen", 0) for _, v in last_n), 2), "margen": round(total_ingreso - total_coste, 2),
"n_days": len(last_n), "n_days": len(last_n),
} }
def _trunc(text: str, limit: int = 2950) -> str:
return text if len(text) <= limit else text[:limit - 3] + ""
def _fmt_eur(v: float) -> str: def _fmt_eur(v: float) -> str:
sign = "+" if v > 0 else "" sign = "+" if v > 0 else ""
return f"{sign}{v:,.0f}".replace(",", ".") return f"{sign}{v:,.0f}".replace(",", ".")
@ -195,7 +201,7 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N
"type": "section", "type": "section",
"text": { "text": {
"type": "mrkdwn", "type": "mrkdwn",
"text": f"📅 *MÁRGENES POR DÍA — {MESES_ES[now.month].upper()}*\n```\n" + "\n".join(rows) + "\n```", "text": _trunc(f"📅 *MÁRGENES POR DÍA — {MESES_ES[now.month].upper()}*\n```\n" + "\n".join(rows) + "\n```"),
}, },
} }
@ -289,14 +295,14 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N
"type": "section", "type": "section",
"text": { "text": {
"type": "mrkdwn", "type": "mrkdwn",
"text": _ranking_last5_text(worst_last5, "📉 *PEOR — ÚLTIMOS 5 DÍAS*"), "text": _trunc(_ranking_last5_text(worst_last5, "📉 *PEOR — ÚLTIMOS 5 DÍAS*")),
}, },
}) })
blocks.append({ blocks.append({
"type": "section", "type": "section",
"text": { "text": {
"type": "mrkdwn", "type": "mrkdwn",
"text": _ranking_last5_text(best_last5, "📈 *MEJOR — ÚLTIMOS 5 DÍAS*"), "text": _trunc(_ranking_last5_text(best_last5, "📈 *MEJOR — ÚLTIMOS 5 DÍAS*")),
}, },
}) })
@ -304,16 +310,17 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N
blocks.append({"type": "divider"}) blocks.append({"type": "divider"})
blocks.append({ blocks.append({
"type": "section", "type": "section",
"fields": [ "text": {
{ "type": "mrkdwn",
"type": "mrkdwn", "text": _trunc(_ranking_month_text(worst_month, "📉 *PEOR — MES EN CURSO*")),
"text": _ranking_month_text(worst_month, "📉 *PEOR — MES EN CURSO*"), },
}, })
{ blocks.append({
"type": "mrkdwn", "type": "section",
"text": _ranking_month_text(best_month, "📈 *MEJOR — MES EN CURSO*"), "text": {
}, "type": "mrkdwn",
], "text": _trunc(_ranking_month_text(best_month, "📈 *MEJOR — MES EN CURSO*")),
},
}) })
if portfolio_analysis_text: if portfolio_analysis_text:
@ -322,7 +329,7 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N
"type": "section", "type": "section",
"text": { "text": {
"type": "mrkdwn", "type": "mrkdwn",
"text": f"🤖 *DIAGNÓSTICO ESTRATÉGICO*\n{portfolio_analysis_text}", "text": _trunc(f"🤖 *DIAGNÓSTICO ESTRATÉGICO*\n{portfolio_analysis_text}"),
}, },
}) })