fix(gemini): usar modelos flash de alta capacidad y fallback ante limite de cuota

This commit is contained in:
AI Bot 2026-08-24 16:00:03 -05:00
parent 8d526bea18
commit 1a56049a43

View File

@ -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 => `<p>${l}</p>`).join('');
const contenido = paragraphs || `<p>${clean || rawTitle}</p>`;
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<ProcessedPublication> {
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);
}