const axios = require('axios'); require("dotenv").config(); const logger = require('./logger.services'); const API_URL = process.env.GEMINI_API_URL; const MAX_RETRIES = 3; const INITIAL_RETRY_DELAY = 1000; const MAX_RETRY_DELAY = 10000; exports.getAIResponse = async (prompt) => { let limitedPrompt = prompt; if (Array.isArray(prompt)) { limitedPrompt = prompt.slice(0, 7); } const promptString = typeof limitedPrompt === "string" ? limitedPrompt : JSON.stringify(limitedPrompt, null, 2); const systemInstruction = ` Anda adalah AI marketing expert yang bertugas menganalisis data aktivitas pengguna dan membuat notifikasi persuasif untuk re-engagement. Instruksi: 1. Analisis pola dan minat pengguna dari data aktivitas JSON yang diberikan. 2. Identifikasi kategori atau fitur yang paling sering diakses (hotel, restoran, transportasi, dll). 3. Buat satu notifikasi yang MENARIK dan PERSUASIF agar pengguna kembali membuka aplikasi. 4. Gunakan gaya bahasa Indonesia yang casual, friendly, dan menggugah rasa penasaran. 5. Fokus pada benefit atau value proposition untuk pengguna, bukan sekadar ringkasan. 6. Notifikasi harus fokus pada re-engagement. 7. Notifikasi harus sesuai dengan skema JSON yang ditentukan (title maks 50 karakter, description maks 100 karakter). JANGAN tambahkan teks, markdown, atau penjelasan apapun di luar JSON murni. `; const responseSchema = { type: "OBJECT", properties: { "title": { "type": "STRING", "description": "Judul yang menggugah & menarik (maksimal 50 karakter)" }, "description": { "type": "STRING", "description": "Deskripsi persuasif dengan CTA yang jelas (maksimal 100 karakter)" } }, required: ["title", "description"] }; const payload = { contents: [{ role: "user", parts: [{ text: `Data aktivitas pengguna untuk dianalisis:\n${promptString}` }] }], systemInstruction: { parts: [{ text: systemInstruction }] }, generationConfig: { responseMimeType: "application/json", responseSchema: responseSchema, temperature: 0.8, maxOutputTokens: 2048, } }; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { const response = await axios.post(API_URL, payload, { headers: { 'Content-Type': 'application/json' }, }); const result = response.data; const jsonText = result.candidates?.[0]?.content?.parts?.[0]?.text; if (!jsonText) { return { error: true, message: "Gemini API returned an unexpected response structure or no text content.", raw: result }; } let responseJson; try { responseJson = JSON.parse(jsonText); } catch (parseErr) { return { error: true, message: "Failed to parse AI response as JSON.", raw: jsonText }; } if ( typeof responseJson !== "object" || !responseJson.title || !responseJson.description || typeof responseJson.title !== "string" || typeof responseJson.description !== "string" ) { return { error: true, message: "AI response does not match expected schema.", raw: responseJson }; } if (responseJson.title.length > 50 || responseJson.description.length > 100) { return { error: true, message: "AI response fields exceed max length.", raw: responseJson }; } return responseJson; } catch (error) { const isLastAttempt = attempt === MAX_RETRIES - 1; const status = error.response?.status; const shouldRetry = !isLastAttempt && ( !status || status === 429 || status === 503 || status >= 500 ); if (shouldRetry) { const exponentialDelay = Math.min( INITIAL_RETRY_DELAY * Math.pow(2, attempt), MAX_RETRY_DELAY ); const jitter = Math.random() * 1000; const delay = exponentialDelay + jitter; logger.warn(`AI Service: Attempt ${attempt + 1}/${MAX_RETRIES} failed${status ? ` (HTTP ${status})` : ''}. Retrying in ${Math.round(delay / 1000)}s...`); await new Promise(resolve => setTimeout(resolve, delay)); continue; } const errorMessage = status ? `API call failed with status ${status}: ${error.response?.data?.error?.message || 'Unknown error'}` : `Network error: ${error.message}`; logger.error(`AI Service: Failed after ${attempt + 1} attempts: ${errorMessage}`); return { error: true, message: errorMessage, raw: error.response?.data || error.message, attempts: attempt + 1 }; } } return { error: true, message: "Exhausted all retries without a successful API response.", raw: null, attempts: MAX_RETRIES }; }; exports.predictOptimalDeliveryTime = async (userActivityData) => { const activityString = JSON.stringify(userActivityData, null, 2); const systemInstruction = ` Anda adalah AI specialist yang menganalisis pola perilaku pengguna untuk menentukan waktu OPTIMAL pengiriman notifikasi. TUGAS ANDA: 1. Analisis data aktivitas pengguna (waktu login, frekuensi penggunaan, pola harian/mingguan) 2. Identifikasi jam-jam PEAK engagement pengguna (kapan user paling aktif) 3. Prediksi waktu terbaik untuk mengirim notifikasi agar dibaca dan di-klik 4. Pertimbangkan: - Pola aktivitas historis (jam berapa user biasa online) - Hari dalam seminggu (weekday vs weekend berbeda) - Interval ideal antar notifikasi (jangan spam) - Timezone dan kebiasaan lokal (pagi, siang, sore, malam) 5. Berikan rekomendasi waktu dalam format ISO 8601 dengan timezone 6. Sertakan confidence score (0-100) dan reasoning singkat ATURAN PENTING: - Jika user aktif pagi (06:00-10:00) → kirim pagi hari berikutnya - Jika user aktif siang (11:00-15:00) → kirim saat lunch break - Jika user aktif sore/malam (16:00-22:00) → kirim sore hari - Jika tidak ada pola jelas → default ke jam 09:00 atau 19:00 (high engagement hours) - Minimum delay: 30 menit dari sekarang - Maximum delay: 24 jam dari sekarang - Hindari jam tidur (23:00-05:00) OUTPUT harus JSON murni tanpa markdown atau teks tambahan. `; const currentTime = new Date(); const responseSchema = { type: "OBJECT", properties: { "optimalDeliveryTime": { "type": "STRING", "description": "Waktu optimal pengiriman dalam ISO 8601 format (contoh: 2024-12-08T19:00:00+07:00)" }, "confidenceScore": { "type": "NUMBER", "description": "Confidence score prediksi (0-100)" }, "reasoning": { "type": "STRING", "description": "Penjelasan singkat mengapa waktu ini dipilih (maks 150 karakter)" }, "userEngagementPattern": { "type": "STRING", "description": "Pola engagement user: 'morning_active', 'afternoon_active', 'evening_active', 'night_active', atau 'irregular'" }, "delayMinutes": { "type": "NUMBER", "description": "Delay dalam menit dari sekarang (minimal 30, maksimal 1440)" } }, required: ["optimalDeliveryTime", "confidenceScore", "reasoning", "userEngagementPattern", "delayMinutes"] }; const payload = { contents: [{ role: "user", parts: [{ text: `Waktu sekarang: ${currentTime.toISOString()}\n\nData aktivitas pengguna:\n${activityString}\n\nTentukan waktu OPTIMAL untuk mengirim notifikasi re-engagement.` }] }], systemInstruction: { parts: [{ text: systemInstruction }] }, generationConfig: { responseMimeType: "application/json", responseSchema: responseSchema, temperature: 0.7, maxOutputTokens: 2048, } }; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { const response = await axios.post(API_URL, payload, { headers: { 'Content-Type': 'application/json' }, }); const result = response.data; const jsonText = result.candidates?.[0]?.content?.parts?.[0]?.text; if (!jsonText) { return { error: true, message: "Gemini API returned unexpected response for predictive timing.", raw: result }; } let responseJson; try { responseJson = JSON.parse(jsonText); } catch (parseErr) { return { error: true, message: "Failed to parse predictive timing response as JSON.", raw: jsonText }; } if ( !responseJson.optimalDeliveryTime || typeof responseJson.confidenceScore !== "number" || !responseJson.reasoning || !responseJson.userEngagementPattern || typeof responseJson.delayMinutes !== "number" ) { return { error: true, message: "Predictive timing response missing required fields.", raw: responseJson }; } if (responseJson.delayMinutes < 30 || responseJson.delayMinutes > 1440) { responseJson.delayMinutes = Math.max(30, Math.min(1440, responseJson.delayMinutes)); } const deliveryTime = new Date(responseJson.optimalDeliveryTime); if (deliveryTime <= currentTime) { const fallbackTime = new Date(currentTime.getTime() + 60 * 60 * 1000); responseJson.optimalDeliveryTime = fallbackTime.toISOString(); responseJson.delayMinutes = 60; responseJson.reasoning = "Adjusted to future time (1 hour from now)"; } logger.info(`AI Predictive Timing: Scheduled for ${responseJson.optimalDeliveryTime} (${responseJson.delayMinutes} min delay, ${responseJson.confidenceScore}% confidence)`); return responseJson; } catch (error) { const isLastAttempt = attempt === MAX_RETRIES - 1; const status = error.response?.status; const shouldRetry = !isLastAttempt && ( !status || status === 429 || status === 503 || status >= 500 ); if (shouldRetry) { const delay = Math.min( INITIAL_RETRY_DELAY * Math.pow(2, attempt) + Math.random() * 1000, MAX_RETRY_DELAY ); logger.warn(`Predictive Timing: Retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay/1000)}s`); await new Promise(resolve => setTimeout(resolve, delay)); continue; } logger.error(`Predictive Timing failed: ${error.message}`); const fallbackTime = new Date(currentTime.getTime() + 2 * 60 * 60 * 1000); return { optimalDeliveryTime: fallbackTime.toISOString(), confidenceScore: 50, reasoning: "AI unavailable, using default 2-hour delay", userEngagementPattern: "unknown", delayMinutes: 120, aiError: true }; } } const fallbackTime = new Date(currentTime.getTime() + 2 * 60 * 60 * 1000); return { optimalDeliveryTime: fallbackTime.toISOString(), confidenceScore: 50, reasoning: "Fallback scheduling after retries exhausted", userEngagementPattern: "unknown", delayMinutes: 120, aiError: true }; };