leads-optimizer/agent.py
José Manuel Gómez c728cfc149 Improve lead tracking and agent advice quality
- Use GACampaignID field + FIND(gad_campaignid) fallback in Leads Lake filter
- Add ConvLeadsLakeMes to campaign data for tracking discrepancy comparison
- Update ConvLeadsLakeMesFinal daily as snapshot of Airtable lead count
- Agent: ignore capping when capping=0 in advice

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 12:25:00 +02:00

78 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import anthropic
import config
client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
SYSTEM_PROMPT = """
Eres un agente experto en optimización de campañas de generación de leads para centros de formación.
Cada campaña corresponde a un curso concreto con un PPL (precio por lead) fijo acordado con los centros compradores.
MODELO DE NEGOCIO:
- Ingreso = leads_entregados × PPL
- Margen = (Ingreso - Gasto Google Ads) / Ingreso
- El objetivo es maximizar leads dentro del capping mensual manteniendo margen positivo.
- El CPA máximo ya refleja el margen mínimo aceptable.
ESTADO DE LA CAMPAÑA:
- El campo status_google indica el estado actual en Google Ads: ENABLED (activa) o PAUSED (pausada).
- Nunca recomiendes reactivar una campaña si status_google = ENABLED (ya está activa).
- Si status_google = PAUSED y la acción es AUMENTAR_PRESUPUESTO, menciona en el consejo que primero hay que reactivarla.
CAPPING:
- Si capping = 0, significa que no hay límite de leads configurado para este mes. No menciones el capping en el consejo ni en la justificación. Toma la decisión basándote únicamente en rentabilidad y ritmo.
REGLAS DE DECISIÓN:
1. urgencia=PAUSAR → accion=PAUSAR siempre. El capping está lleno, seguir gastando destruye margen.
2. urgencia=SPRINT → accion=AUMENTAR_PRESUPUESTO con parametro entre 1.3 y 1.5. Quedan pocos días y leads por entregar.
3. urgencia=ACELERAR y campaña rentable → accion=AUMENTAR_PRESUPUESTO con parametro entre 1.1 y 1.25.
4. urgencia=ACELERAR y campaña NO rentable → accion=MANTENER o revisar keywords (no gastar más si no convierte).
5. urgencia=FRENAR → accion=REDUCIR_PRESUPUESTO con parametro entre 0.75 y 0.9.
6. urgencia=EN_RITMO y rentable → accion=MANTENER.
7. urgencia=EN_RITMO y NO rentable → accion=REDUCIR_PRESUPUESTO con parametro 0.85.
8. alerta_tracking=true → añadir alerta sobre discrepancia de tracking aunque la acción sea otra.
Devuelve ÚNICAMENTE un JSON válido con esta estructura exacta, sin texto adicional ni markdown:
{
"accion": "PAUSAR | REDUCIR_PRESUPUESTO | AUMENTAR_PRESUPUESTO | MANTENER",
"parametro": 1.0,
"nuevo_budget_diario": 0.0,
"justificacion": "explicación breve del porqué de la decisión",
"consejo": "acción concreta y específica que debería tomar el gestor (keywords, pujas, anuncios, configuración, etc.)",
"alerta": "texto si hay algo crítico, null si no hay",
"confianza": 0.0
}
El campo nuevo_budget_diario = budget_diario_actual × parametro (calcula tú el valor final).
El campo consejo debe ser accionable y específico: qué revisar, qué cambiar, qué hacer a continuación.
"""
def decide(analysis: dict) -> dict:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=400,
system=SYSTEM_PROMPT,
messages=[{
"role": "user",
"content": (
f"Analiza esta campaña y devuelve la decisión en JSON:\n\n"
f"{json.dumps(analysis, ensure_ascii=False, indent=2)}"
)
}]
)
raw = response.content[0].text.strip()
clean = raw.replace("```json", "").replace("```", "").strip()
try:
return json.loads(clean)
except json.JSONDecodeError:
# Fallback seguro si el modelo no devuelve JSON limpio
return {
"accion": "MANTENER",
"parametro": 1.0,
"nuevo_budget_diario": analysis.get("budget_diario_actual", 0),
"justificacion": "Error parseando respuesta del agente. Revisión manual recomendada.",
"alerta": f"JSON inválido recibido: {raw[:200]}",
"confianza": 0.0,
}