diff --git a/trip-service/server.js b/trip-service/server.js index f33b315..243fce0 100644 --- a/trip-service/server.js +++ b/trip-service/server.js @@ -109,6 +109,93 @@ async function getAiInsight(activityDetails, location, modelName = 'qwen2.5:0.5b }); } +async function generateAiSchedule(activities, meals, date, trip) { + return new Promise((resolve) => { + const prompt = `You are a strict JSON API scheduling agent. Extract a daily schedule from these plans: +Activities: ${activities} +Meals: ${meals} +Date: ${date} + +Return ONLY a JSON array of objects. Each object must have "time" (in 24-hour HH:mm format) and "message" (a short, fun 1-sentence reminder). Estimate reasonable times if none are given (e.g. breakfast 8:00, lunch 12:30, dinner 18:30). DO NOT output any markdown or conversational text.`; + + const modelName = trip.llmModel || 'qwen2.5:0.5b'; + // Support future providers via trip.llmProvider + // Defaulting to Ollama for now + const body = JSON.stringify({ model: modelName, prompt, stream: false }); + const url = new URL(OLLAMA_URL); + + const req = http.request({ + hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } + }, (res) => { + let d = ''; + res.on('data', c => d += c); + res.on('end', () => { + try { + const raw = JSON.parse(d).response; + // Extract JSON array using regex in case of conversational wrapper + const match = raw.match(/\[[\s\S]*\]/); + if (match) { + resolve(JSON.parse(match[0])); + } else { + resolve([]); + } + } catch (e) { + console.error('[TripService] AI Schedule parsing failed:', e.message); + resolve([]); + } + }); + }); + req.on('error', () => resolve([])); + req.write(body); + req.end(); + }); +} + +function scheduleRemindersBatch(events, trip, dateStr) { + if (!events || events.length === 0) return; + + // Base date object for today in Eastern Time (approximated locally) + const baseDate = new Date(dateStr + "T00:00:00-04:00"); + + const items = events.map(ev => { + const [hh, mm] = ev.time.split(':'); + const eventTime = new Date(baseDate); + eventTime.setHours(parseInt(hh, 10)); + eventTime.setMinutes(parseInt(mm, 10)); + + // Send reminder 15 minutes before the event time + const sendAt = new Date(eventTime.getTime() - 15 * 60000); + + // Don't schedule in the past + if (sendAt <= new Date()) return null; + + return { + message: `⏰ *${trip.name} Schedule Alert*\n\n${ev.message}\n\n_Starts at ${ev.time}_`, + sendAt: sendAt.toISOString(), + label: `${trip.name} - ${ev.message.slice(0, 30)}...`, + to: trip.sendToGroup && trip.groupUrls && trip.groupUrls.length > 0 ? trip.groupUrls[0] : (GROUP_URL || PERSONAL_NUM) + }; + }).filter(Boolean); + + if (items.length === 0) return; + + const body = JSON.stringify({ items }); + const req = http.request({ + hostname: 'reminder-service.ai-agents.svc.cluster.local', + port: 6001, + path: '/reminders/batch', + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } + }, (res) => { + res.on('data', () => {}); + res.on('end', () => console.log(`[TripService] Batch queued ${items.length} reminders via reminder-service`)); + }); + req.on('error', (e) => console.error('[TripService] Failed to queue reminders:', e.message)); + req.write(body); + req.end(); +} + function sendWhatsApp(to, message) { return new Promise((resolve) => { const body = JSON.stringify({ to, message }); @@ -155,6 +242,15 @@ async function generateAndSendBriefing(trip, dayPlan) { for (const group of groupsToNotify) { await sendWhatsApp(group, message); } + + // Generate and schedule detailed AI itinerary + console.log(`[TripService] Generating detailed AI schedule for ${trip.name} - ${dayPlan.date}`); + const scheduleEvents = await generateAiSchedule(dayPlan.activities, dayPlan.meals, dayPlan.date, trip); + if (scheduleEvents.length > 0) { + scheduleRemindersBatch(scheduleEvents, trip, dayPlan.date); + } else { + console.log(`[TripService] AI yielded no parseable schedule events.`); + } } // ── Routes ───────────────────────────────────────────────────────────────────