mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
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:
+54
-2
@@ -72,13 +72,27 @@ JarvisDB::insert(
|
|||||||
[$sessionId, 'user', $message]
|
[$sessionId, 'user', $message]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Conversation history
|
// Conversation history — current session
|
||||||
$history = JarvisDB::query(
|
$history = JarvisDB::query(
|
||||||
'SELECT role, content FROM conversations WHERE session_id=? ORDER BY created_at DESC LIMIT 10',
|
'SELECT role, content FROM conversations WHERE session_id=? ORDER BY created_at DESC LIMIT 10',
|
||||||
[$sessionId]
|
[$sessionId]
|
||||||
);
|
) ?? [];
|
||||||
$history = array_reverse($history);
|
$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;
|
$reply = null;
|
||||||
$source = 'unknown';
|
$source = 'unknown';
|
||||||
|
|
||||||
@@ -1633,6 +1647,44 @@ if (!$reply) {
|
|||||||
if ($matched && $matched['action'] === 'action') {
|
if ($matched && $matched['action'] === 'action') {
|
||||||
switch ($matched['intent']) {
|
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':
|
case 'focus_mode':
|
||||||
$uiAction = 'focus_mode';
|
$uiAction = 'focus_mode';
|
||||||
$reply = "Focus mode activated, {$userAddr}. Side panels hidden.";
|
$reply = "Focus mode activated, {$userAddr}. Side panels hidden.";
|
||||||
|
|||||||
@@ -2584,6 +2584,8 @@ function showApp(name, greeting, silent = false) {
|
|||||||
loadWeather();
|
loadWeather();
|
||||||
loadNews();
|
loadNews();
|
||||||
initMobile();
|
initMobile();
|
||||||
|
setTimeout(checkPlannerReminder, 3000);
|
||||||
|
setInterval(checkUpcomingAppts, 300000);
|
||||||
// Guardian Mode — badge refresh + proactive chat
|
// Guardian Mode — badge refresh + proactive chat
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
_refreshGuardianBadge();
|
_refreshGuardianBadge();
|
||||||
@@ -3157,6 +3159,52 @@ async function toggleHA(entityId, domain, currentState) {
|
|||||||
} catch(e) {}
|
} 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) ─────────────────────────────────
|
// ── PLANNER SUMMARY (top bar badge only) ─────────────────────────────────
|
||||||
async function loadPlannerSummary() {
|
async function loadPlannerSummary() {
|
||||||
const d = await api('planner/today');
|
const d = await api('planner/today');
|
||||||
|
|||||||
Reference in New Issue
Block a user