feat: voice agent commands, cross-session memory, proactive reminders

- Voice commands (#3): say "restart the homebridge agent" or "restart mediastack agent" → queues restart_service to that agent; lists options if no hostname matched; 6 KB intents added
- Persistent context (#4): chat.php now loads last 6 turns from most recent prior session before current session history, giving JARVIS memory across page reloads
- Proactive reminders (#5): 3s after login, auto-announces overdue tasks / tasks due today / upcoming appointments; 5-min interval checks for appointments starting within 15 min and speaks alert once

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 02:23:48 +00:00
parent c74a9af8be
commit e381858299
2 changed files with 102 additions and 2 deletions
+54 -2
View File
@@ -72,13 +72,27 @@ JarvisDB::insert(
[$sessionId, 'user', $message]
);
// Conversation history
// Conversation history — current session
$history = JarvisDB::query(
'SELECT role, content FROM conversations WHERE session_id=? ORDER BY created_at DESC LIMIT 10',
[$sessionId]
);
) ?? [];
$history = array_reverse($history);
// Cross-session memory: last 6 turns from the most recent prior session
$priorSession = JarvisDB::query(
"SELECT session_id FROM conversations WHERE session_id != ? AND role='user'
ORDER BY created_at DESC LIMIT 1",
[$sessionId]
);
if ($priorSession && !empty($priorSession[0]['session_id'])) {
$priorHistory = JarvisDB::query(
'SELECT role, content FROM conversations WHERE session_id=? ORDER BY created_at DESC LIMIT 6',
[$priorSession[0]['session_id']]
) ?? [];
$history = array_merge(array_reverse($priorHistory), $history);
}
$reply = null;
$source = 'unknown';
@@ -1633,6 +1647,44 @@ if (!$reply) {
if ($matched && $matched['action'] === 'action') {
switch ($matched['intent']) {
case 'restart_agent':
// Extract target hostname from message
$msgLow = strtolower($message);
$agentMap = [
'homebridge' => 'homebridge_b57cbaea',
'jellyfin' => 'jellyfin_7e386833',
'networkbackup' => 'networkbackup_NetworkB',
'network backup'=> 'networkbackup_NetworkB',
'novacpx' => 'novacpx_e3b07264',
'nova' => 'novacpx_e3b07264',
'mediastack' => 'MediaStack_2c00b1b8',
'media stack' => 'MediaStack_2c00b1b8',
'homeassistant' => 'homeassistant_ha',
'home assistant'=> 'homeassistant_ha',
];
$targetAgentId = null;
$targetName = null;
foreach ($agentMap as $keyword => $agentId) {
if (str_contains($msgLow, $keyword)) {
$targetAgentId = $agentId;
$targetName = ucfirst($keyword);
break;
}
}
if ($targetAgentId) {
JarvisDB::insert(
"INSERT INTO agent_commands (agent_id, command_type, command_data, status, created_at)
VALUES (?, 'restart_service', ?, 'pending', NOW())",
[$targetAgentId, json_encode(['service' => 'jarvis-agent'])]
);
$reply = "Restart command sent to the {$targetName} agent, {$userAddr}. It should come back online within 15 seconds.";
} else {
// Fall back to listing restartable agents
$reply = "Which agent should I restart, {$userAddr}? I can restart: HomeAssistant, Homebridge, Jellyfin, MediaStack, NetworkBackup, or NovaCPX.";
}
$source = 'intent:restart_agent';
break;
case 'focus_mode':
$uiAction = 'focus_mode';
$reply = "Focus mode activated, {$userAddr}. Side panels hidden.";
+48
View File
@@ -2584,6 +2584,8 @@ function showApp(name, greeting, silent = false) {
loadWeather();
loadNews();
initMobile();
setTimeout(checkPlannerReminder, 3000);
setInterval(checkUpcomingAppts, 300000);
// Guardian Mode — badge refresh + proactive chat
setTimeout(() => {
_refreshGuardianBadge();
@@ -3157,6 +3159,52 @@ async function toggleHA(entityId, domain, currentState) {
} catch(e) {}
}
// ── PROACTIVE REMINDERS ──────────────────────────────────────────────────────
let _reminderShown = false;
async function checkPlannerReminder() {
if (_reminderShown || sessionStorage.getItem('reminderShown')) return;
_reminderShown = true;
sessionStorage.setItem('reminderShown', '1');
const d = await api('planner/today').catch(() => null);
if (!d) return;
const tasks = [...(d.tasks_overdue||[]), ...(d.tasks_today||[])];
const appts = d.appts_today || [];
const overdue = d.tasks_overdue?.length || 0;
if (!tasks.length && !appts.length) return;
const parts = [];
if (overdue) parts.push(overdue + ' overdue task' + (overdue > 1 ? 's' : ''));
if (tasks.length - overdue > 0) parts.push((tasks.length - overdue) + ' task' + (tasks.length - overdue > 1 ? 's' : '') + ' due today');
if (appts.length) {
const nextAppt = appts[0];
const t = nextAppt.start_at ? new Date(nextAppt.start_at).toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true}) : '';
parts.push((t ? 'appointment at ' + t : appts.length + ' appointment' + (appts.length > 1 ? 's' : '') + ' today'));
}
const msg = 'Heads up, ' + (sessionUser||'Sir') + '. You have ' + parts.join(' and ') + '.';
addMessage('jarvis', msg);
if (typeof speak === 'function' && isVoiceActive) speak(msg);
}
// Check for upcoming appointments (fires every 5 min after load)
let _apptAlerted = new Set();
async function checkUpcomingAppts() {
const d = await api('planner/today').catch(() => null);
if (!d) return;
const now = Date.now();
for (const a of (d.appts_today||[])) {
if (!a.start_at || _apptAlerted.has(a.id)) continue;
const start = new Date(a.start_at).getTime();
const minsUntil = (start - now) / 60000;
if (minsUntil > 0 && minsUntil <= 15) {
_apptAlerted.add(a.id);
const msg = 'Reminder: ' + a.title + ' starts in ' + Math.round(minsUntil) + ' minutes' + (a.location ? ' at ' + a.location : '') + '.';
addMessage('jarvis', msg);
if (typeof speak === 'function' && isVoiceActive) speak(msg);
}
}
}
// ── PLANNER SUMMARY (top bar badge only) ─────────────────────────────────
async function loadPlannerSummary() {
const d = await api('planner/today');