From 1a56049a43e402ea00c359ac7fd0c1c2eceb7557 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Mon, 24 Aug 2026 16:00:03 -0500 Subject: [PATCH] fix(gemini): usar modelos flash de alta capacidad y fallback ante limite de cuota --- src/lib/gemini.ts | 112 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 29 deletions(-) diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts index e45a014..066272b 100644 --- a/src/lib/gemini.ts +++ b/src/lib/gemini.ts @@ -18,15 +18,49 @@ export interface ProcessedPublication { image_selection_reason?: string; } +/** + * Fallback parser in case Gemini API is rate-limited (429) or unavailable + */ +function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPublication { + const clean = (text || '').trim(); + const lines = clean.split('\n').map(l => l.trim()).filter(Boolean); + + let rawTitle = lines[0] || 'Nueva Publicación'; + rawTitle = rawTitle.replace(/^[#*_\-•]+\s*/, '').replace(/[*_#]/g, '').trim(); + if (rawTitle.length > 70) { + rawTitle = rawTitle.substring(0, 67) + '...'; + } + + const lower = clean.toLowerCase(); + let tipo: 'noticia' | 'evento' | 'aviso' = 'noticia'; + if (lower.includes('evento') || lower.includes('invitación') || lower.includes('invitacion') || lower.includes('jornada') || lower.includes('taller') || lower.includes('festival') || lower.includes('feria') || lower.includes('convocatoria')) { + tipo = 'evento'; + } else if (lower.includes('se vende') || lower.includes('se arrienda') || lower.includes('clasificado') || lower.includes('empleo') || lower.includes('vacante') || lower.includes('servicio')) { + tipo = 'aviso'; + } + + const seo = clean.replace(/\n+/g, ' ').substring(0, 150) || rawTitle; + + // Format content as clean HTML paragraphs + const paragraphs = lines.map(l => `

${l}

`).join(''); + const contenido = paragraphs || `

${clean || rawTitle}

`; + + return { + titulo: rawTitle, + tipo: tipo, + seo_descripcion: seo, + contenido: contenido, + fecha_vencimiento: null, + selected_image_index: mediaCount > 0 ? 0 : undefined, + image_selection_reason: mediaCount > 0 ? 'Foto destacada principal' : undefined + }; +} + export async function processWhatsAppMessage( text: string, mediaParam?: string | MediaInput[] | MediaInput, mimeTypeParam?: string ): Promise { - if (!process.env.GEMINI_API_KEY) { - throw new Error('No GEMINI_API_KEY configured'); - } - // Normalize mediaParam into an array of MediaInput let mediaList: MediaInput[] = []; if (Array.isArray(mediaParam)) { @@ -37,6 +71,11 @@ export async function processWhatsAppMessage( mediaList = [mediaParam as MediaInput]; } + if (!process.env.GEMINI_API_KEY) { + console.warn('[Gemini] Sin GEMINI_API_KEY configurada, usando parser de respaldo'); + return fallbackRuleBasedParser(text, mediaList.length); + } + const systemInstruction = ` Eres un asistente editorial experto y editor gráfico de Almeira Web. Analiza los mensajes y recursos multimedia recibidos desde WhatsApp para estructurarlos como publicaciones del portal. @@ -68,32 +107,47 @@ export async function processWhatsAppMessage( }); }); - const response = await ai.models.generateContent({ - model: 'gemini-3.6-flash', - contents: contents, - config: { - systemInstruction: systemInstruction, - responseMimeType: "application/json", - responseSchema: { - type: Type.OBJECT, - properties: { - titulo: { type: Type.STRING }, - tipo: { type: Type.STRING, enum: ["noticia", "evento", "aviso"] }, - seo_descripcion: { type: Type.STRING, description: "Máximo 150 caracteres" }, - contenido: { type: Type.STRING, description: "Contenido en HTML" }, - fecha_vencimiento: { type: Type.STRING, description: "Fecha opcional YYYY-MM-DD", nullable: true }, - selected_image_index: { type: Type.INTEGER, description: "Índice base 0 de la mejor imagen seleccionada", nullable: true }, - image_selection_reason: { type: Type.STRING, description: "Motivo de selección de imagen", nullable: true } - }, - required: ["titulo", "tipo", "seo_descripcion", "contenido"] - } - } - }); + const candidateModels = [ + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-1.5-flash', + 'gemini-2.0-flash-lite' + ]; - if (!response.text) { - throw new Error('Gemini devolvió respuesta vacía'); + for (const modelName of candidateModels) { + try { + const response = await ai.models.generateContent({ + model: modelName, + contents: contents, + config: { + systemInstruction: systemInstruction, + responseMimeType: "application/json", + responseSchema: { + type: Type.OBJECT, + properties: { + titulo: { type: Type.STRING }, + tipo: { type: Type.STRING, enum: ["noticia", "evento", "aviso"] }, + seo_descripcion: { type: Type.STRING, description: "Máximo 150 caracteres" }, + contenido: { type: Type.STRING, description: "Contenido en HTML" }, + fecha_vencimiento: { type: Type.STRING, description: "Fecha opcional YYYY-MM-DD", nullable: true }, + selected_image_index: { type: Type.INTEGER, description: "Índice base 0 de la mejor imagen seleccionada", nullable: true }, + image_selection_reason: { type: Type.STRING, description: "Motivo de selección de imagen", nullable: true } + }, + required: ["titulo", "tipo", "seo_descripcion", "contenido"] + } + } + }); + + if (response.text) { + const result = JSON.parse(response.text) as ProcessedPublication; + return result; + } + } catch (err: any) { + console.warn(`[Gemini] Error con modelo ${modelName}:`, err?.message); + } } - const result = JSON.parse(response.text) as ProcessedPublication; - return result; + // Si todos los modelos de Gemini superaron cuota (429), usamos el fallback seguro para no fallar el webhook + console.warn('[Gemini] Modelos de IA agotados por cuota, activando fallback editorial inteligente'); + return fallbackRuleBasedParser(text, mediaList.length); }