diff --git a/agents/k8s/reminder-service-deployment.yaml b/agents/k8s/reminder-service-deployment.yaml new file mode 100644 index 0000000..14c1775 --- /dev/null +++ b/agents/k8s/reminder-service-deployment.yaml @@ -0,0 +1,99 @@ +--- +# PVC for persistent reminder storage +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: reminder-service-pvc + namespace: ai-agents +spec: + accessModes: [ReadWriteOnce] + storageClassName: longhorn + resources: + requests: + storage: 512Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: reminder-service + namespace: ai-agents + labels: + app: reminder-service +spec: + replicas: 1 + selector: + matchLabels: + app: reminder-service + template: + metadata: + labels: + app: reminder-service + spec: + initContainers: + - name: git-clone + image: alpine/git + command: + - git + - clone + - http://192.168.8.248:3000/deepkoluguri/agentic-os.git + - /app + volumeMounts: + - name: app-volume + mountPath: /app + containers: + - name: reminder-service + image: node:18-alpine + workingDir: /app/reminder-service + command: + - sh + - -c + - npm install --omit=dev && node server.js + ports: + - containerPort: 6001 + env: + - name: DATA_FILE + value: /data/reminders.json + - name: PORT + value: "6001" + - name: GATEWAY_URL + value: http://whatsapp-gateway.ai-agents.svc.cluster.local:5001/send-message + - name: PERSONAL_NUM + value: "+14085505485" + - name: GROUP_URL + value: "https://chat.whatsapp.com/0Txfg7iZBbvIuvTbTqqgAX" + volumeMounts: + - name: app-volume + mountPath: /app + - name: data-volume + mountPath: /data + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + readinessProbe: + httpGet: + path: /health + port: 6001 + initialDelaySeconds: 30 + periodSeconds: 15 + volumes: + - name: app-volume + emptyDir: {} + - name: data-volume + persistentVolumeClaim: + claimName: reminder-service-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: reminder-service + namespace: ai-agents +spec: + selector: + app: reminder-service + ports: + - port: 6001 + targetPort: 6001 diff --git a/interface/hq-dashboard/app/api/reminders/route.js b/interface/hq-dashboard/app/api/reminders/route.js new file mode 100644 index 0000000..3f46999 --- /dev/null +++ b/interface/hq-dashboard/app/api/reminders/route.js @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server'; + +const REMINDER_SVC = process.env.REMINDER_SERVICE_URL || + 'http://reminder-service.ai-agents.svc.cluster.local:6001'; + +// GET /api/reminders — list reminders (optional ?status=pending) +export async function GET(request) { + try { + const { searchParams } = new URL(request.url); + const status = searchParams.get('status') || ''; + const url = `${REMINDER_SVC}/reminders${status ? `?status=${status}` : ''}`; + const res = await fetch(url, { next: { revalidate: 0 } }); + const data = await res.json(); + return NextResponse.json(data); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} + +// POST /api/reminders/batch — schedule multiple at once +export async function POST(request) { + try { + const body = await request.json(); + const res = await fetch(`${REMINDER_SVC}/reminders/batch`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + const data = await res.json(); + return NextResponse.json(data, { status: res.ok ? 200 : res.status }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} + +// DELETE /api/reminders?id=rem_xxx — cancel a reminder +export async function DELETE(request) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 }); + const res = await fetch(`${REMINDER_SVC}/reminders/${id}`, { method: 'DELETE' }); + const data = await res.json(); + return NextResponse.json(data, { status: res.ok ? 200 : res.status }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/interface/hq-dashboard/app/api/schedule-reminder/route.js b/interface/hq-dashboard/app/api/schedule-reminder/route.js new file mode 100644 index 0000000..b3e24fa --- /dev/null +++ b/interface/hq-dashboard/app/api/schedule-reminder/route.js @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server'; + +const REMINDER_SVC = process.env.REMINDER_SERVICE_URL || + 'http://reminder-service.ai-agents.svc.cluster.local:6001'; + +export async function POST(request) { + try { + const body = await request.json(); + const res = await fetch(`${REMINDER_SVC}/reminders`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + const data = await res.json(); + return NextResponse.json(data, { status: res.ok ? 200 : res.status }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/interface/hq-dashboard/app/page.jsx b/interface/hq-dashboard/app/page.jsx index f167524..c5b1d73 100644 --- a/interface/hq-dashboard/app/page.jsx +++ b/interface/hq-dashboard/app/page.jsx @@ -36,7 +36,7 @@ export default function Page() { const [messages, setMessages] = useState([ { role: "assistant", - content: "Hello! I am Claw, your local agent. Try asking me:\n• `weather` — live weather for Mars, PA\n• `whatsapp me: your message` — send anything to your phone\n• `check k8s pods` — Kubernetes diagnostics\n• `diagnose proxmox container 102` — disk & resource check", + content: "Hello! I am Claw, your local agent. Try:\n• `weather` — live weather for Mars, PA\n• `whatsapp me: message` — send now to your phone\n• `remind me at 3pm: call doctor` — schedule for later\n• `remind me tomorrow at 6am: gym` — future reminders\n• Or give me a list with times and I'll schedule them all!", timestamp: "Just now" } ]); @@ -46,6 +46,7 @@ export default function Page() { const [weatherCard, setWeatherCard] = useState(null); const [weatherSending, setWeatherSending] = useState(false); const [weatherSent, setWeatherSent] = useState(false); + const [scheduledReminders, setScheduledReminders] = useState([]); // OpsAgent State const [pendingApprovals, setPendingApprovals] = useState([ @@ -151,6 +152,88 @@ export default function Page() { responseText = `❌ Couldn't send to WhatsApp: ${err.message}. Make sure the gateway is running.`; } + // ── Scheduled Reminder ───────────────────────────────────────────────────── + // Patterns: "remind me at X: msg", "schedule (whatsapp) at X: msg", + // "send to whatsapp at X: msg", multi-bullet lists + } else if ( + /(?:remind\s+me|schedule\s+(?:a\s+)?(?:whatsapp\s+)?(?:reminder|message)?)\s+(?:at|on|for|in|tomorrow|monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i.test(userText) || + /send\s+to\s+whatsapp\s+(?:at|in|tomorrow|on\s+)/i.test(userText) || + // multi-bullet list: "schedule these:\n- msg at 3pm\n- msg2 at 6am" + (/(?:schedule|remind\s+me)/i.test(userText) && userText.includes('\n')) + ) { + // Check for multi-item list (lines starting with - or •) + const lines = userText.split('\n').map(l => l.trim()).filter(Boolean); + const bulletLines = lines.filter(l => /^[-•*]\s+/.test(l) || (lines.length > 2 && l !== lines[0])); + const isBatch = bulletLines.length > 1; + + if (isBatch) { + // Batch scheduling + const items = []; + const skipped = []; + for (const line of bulletLines) { + const clean = line.replace(/^[-•*]\s*/, '').trim(); + // Try to split into message + time: "call doctor at 2pm" or "2pm: call doctor" + // Strategy: extract time, rest is the message + const sendAt = parseScheduleDateTime(clean); + if (!sendAt) { skipped.push(clean); continue; } + // Remove the time expression from the label + const label = clean.replace(/(at|by|on|for)?\s*\d{1,2}(?::\d{2})?\s*(am|pm)/gi, '').replace(/tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday/gi, '').replace(/in\s+\d+\s+(hour|hr|minute|min)s?/gi, '').trim().replace(/\s+/g, ' '); + const formatted = `⏰ *Claw Reminder*\n\n${label}\n\n_Scheduled for ${sendAt.toLocaleString('en-US', { timeZone: 'America/New_York', weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })} ET_`; + items.push({ message: formatted, sendAt: sendAt.toISOString(), label }); + } + + if (items.length === 0) { + responseText = `❌ I couldn't parse any valid times from your list. Make sure each item includes a time like "at 3pm", "tomorrow at 9am", or "in 2 hours".`; + } else { + setClawTerminalLogs(prev => [...prev, `[Claw] Scheduling ${items.length} reminders...`]); + try { + const res = await fetch('/api/reminders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items }) }); + const data = await res.json(); + const lines2 = data.created?.map(c => `✅ "${c.label}" → ${formatReminderTime(c.sendAt)} ET`) || []; + if (skipped.length) lines2.push(...skipped.map(s => `⚠️ Skipped (no time): "${s}"`)); + responseText = `📅 Scheduled ${data.created?.length || 0} reminder(s):\n\n${lines2.join('\n')}`; + // Refresh reminder panel + fetch('/api/reminders?status=pending').then(r => r.json()).then(d => { if (Array.isArray(d)) setScheduledReminders(d); }); + setClawTerminalLogs(prev => [...prev, `[Claw] ✅ Batch scheduled: ${items.length} reminders`]); + } catch (err) { + responseText = `❌ Failed to schedule: ${err.message}`; + } + } + + } else { + // Single reminder + // Parse message after colon: "remind me at 3pm: call doctor" → msg = "call doctor" + const colonSplit = userText.match(/[:\u2014]\s*(.+)$/s); + const msgText = colonSplit ? colonSplit[1].trim() : userText; + const sendAt = parseScheduleDateTime(userText); + + if (!sendAt) { + responseText = `❌ I couldn't find a valid time in your message. Try: "remind me at 3pm: call doctor" or "remind me tomorrow at 9am: gym".`; + } else { + const etLabel = sendAt.toLocaleString('en-US', { timeZone: 'America/New_York', weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }); + const waMsg = `⏰ *Claw Reminder*\n\n${msgText}\n\n_Scheduled for ${etLabel} ET_`; + setClawTerminalLogs(prev => [...prev, `[Claw] Scheduling: "${msgText}" → ${sendAt.toISOString()}`]); + try { + const res = await fetch('/api/schedule-reminder', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: waMsg, sendAt: sendAt.toISOString(), label: msgText }), + }); + const data = await res.json(); + if (data.success) { + responseText = `✅ Reminder scheduled!\n\n📌 **"${msgText}"**\n🕐 Delivers at **${etLabel} ET** to your WhatsApp (+14085505485)`; + fetch('/api/reminders?status=pending').then(r => r.json()).then(d => { if (Array.isArray(d)) setScheduledReminders(d); }); + setClawTerminalLogs(prev => [...prev, `[Claw] ✅ Scheduled ID: ${data.id}`]); + } else { + throw new Error(data.error || 'Scheduling failed'); + } + } catch (err) { + responseText = `❌ Couldn't schedule: ${err.message}`; + setClawTerminalLogs(prev => [...prev, `[Claw] ❌ ${err.message}`]); + } + } + } + } else if (cleanInput.includes("weather")) { // Extract optional location override @@ -277,7 +360,108 @@ export default function Page() { } }; - // Approval Handlers + // Poll scheduled reminders every 30s when on claw tab + useEffect(() => { + if (activeTab !== 'claw') return; + const fetchReminders = async () => { + try { + const res = await fetch('/api/reminders?status=pending'); + if (res.ok) { + const data = await res.json(); + if (Array.isArray(data)) setScheduledReminders(data); + } + } catch { /* reminder service may not be up yet */ } + }; + fetchReminders(); + const interval = setInterval(fetchReminders, 30000); + return () => clearInterval(interval); + }, [activeTab]); + + // ── Natural-language date/time parser ──────────────────────────────────────── + const parseScheduleDateTime = (text) => { + const t = text.toLowerCase(); + const now = new Date(); + + // "in X hours/minutes" + const inMatch = t.match(/\bin\s+(\d+)\s+(hour|hr|minute|min)s?\b/); + if (inMatch) { + const n = parseInt(inMatch[1]); + const ms = inMatch[2].startsWith('h') ? n * 3600000 : n * 60000; + return new Date(now.getTime() + ms); + } + + // Extract time component: "3pm", "6:30am", "14:00" + let hours = null, minutes = 0; + const ampm = t.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)/); + const h24 = t.match(/(\d{2}):(\d{2})(?!\s*(am|pm))/); + if (ampm) { + hours = parseInt(ampm[1]); + minutes = ampm[2] ? parseInt(ampm[2]) : 0; + if (ampm[3] === 'pm' && hours !== 12) hours += 12; + if (ampm[3] === 'am' && hours === 12) hours = 0; + } else if (h24) { + hours = parseInt(h24[1]); + minutes = parseInt(h24[2]); + } + + // Build date + let date = new Date(now); + let explicitDate = false; + + if (t.includes('tomorrow')) { + date.setDate(date.getDate() + 1); + explicitDate = true; + } else { + const days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday']; + const dayIdx = days.findIndex(d => t.includes(d)); + if (dayIdx !== -1) { + const diff = ((dayIdx - now.getDay()) + 7) % 7 || 7; // next occurrence + date.setDate(date.getDate() + diff); + explicitDate = true; + } else { + // Named month: "May 30", "June 1" + const months = ['january','february','march','april','may','june','july','august','september','october','november','december']; + const shortM = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']; + let mIdx = months.findIndex(m => t.includes(m)); + if (mIdx === -1) mIdx = shortM.findIndex(m => t.match(new RegExp('\\b' + m + '\\b'))); + if (mIdx !== -1) { + const dayNum = t.match(/(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+(\d{1,2})/i); + if (dayNum) { + date.setMonth(mIdx, parseInt(dayNum[1])); + if (date < now) date.setFullYear(date.getFullYear() + 1); + explicitDate = true; + } + } + } + } + + if (hours === null) return null; // no time found + + date.setHours(hours, minutes, 0, 0); + + // If the computed time is in the past and no explicit date was given, push to tomorrow + if (date <= now && !explicitDate) date.setDate(date.getDate() + 1); + + return date; + }; + + // ── Format a Date nicely for display ───────────────────────────────────────── + const formatReminderTime = (isoStr) => { + const d = new Date(isoStr); + return d.toLocaleString('en-US', { + timeZone: 'America/New_York', + weekday: 'short', month: 'short', day: 'numeric', + hour: 'numeric', minute: '2-digit', hour12: true + }); + }; + + // ── Cancel a reminder ───────────────────────────────────────────────────────── + const handleCancelReminder = async (id) => { + try { + const res = await fetch(`/api/reminders?id=${id}`, { method: 'DELETE' }); + if (res.ok) setScheduledReminders(prev => prev.filter(r => r.id !== id)); + } catch { /* silent */ } + }; const handleApprove = async (id, action, target) => { setPendingApprovals(prev => prev.map(item => item.id === id ? { ...item, status: "approved" } : item)); setOpsLogs(prev => [...prev, `2026-05-29 01:14:10 [OpsAgent] RECEIVED APPROVAL SIGNAL for request ${id}`]); @@ -749,6 +933,7 @@ export default function Page() { {/* Claw Tool & Console Log Panel */}