feat: email intelligence — action item detection, task/appt creation, admin EMAIL tab, full voice intents

This commit is contained in:
2026-05-31 18:57:47 +00:00
parent f122de483a
commit 7c1cfda588
3 changed files with 423 additions and 128 deletions
+101 -54
View File
@@ -300,80 +300,127 @@ if (!$reply && preg_match('/(is|are|what.s|status|state).*(on|off|light|switch|p
}
// ── Email queries ────────────────────────────────────────────────────────
// ── Email + Planner voice intents (action items, create task/appt from email)
if (!$reply && preg_match('/\b(email|emails|inbox|gmail|outlook|mail|unread|messages)\b/i', $message)) {
$emailUrl = (defined('SITE_URL') ? SITE_URL : 'https://jarvis.orbishosting.com') . '/api/email';
$account = 'all';
if (preg_match('/\bgmail\b/i', $message)) $account = 'gmail';
if (preg_match('/\boutlook\b/i', $message)) $account = 'outlook';
if (preg_match('/\bicloud\b/i', $message)) $account = 'icloud';
$lc = strtolower($message);
$ch = curl_init($emailUrl . '?account=' . $account);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-Session-Token: ' . ($_SESSION['jarvis_token'] ?? '')],
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => false,
]);
$emailJson = curl_exec($ch);
$emailCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// ── Action items from email ───────────────────────────────────────────────
if (preg_match('/\b(action item|action required|need.*attention|follow.?up|things to do.*email|email.*task)\b/i', $message)) {
$rows = JarvisDB::query(
"SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL ORDER BY received_at DESC LIMIT 5"
) ?? [];
if (!$rows) {
$reply = "No email action items pending, {$userAddr}.";
} else {
$items = array_map(fn($r) =>
ucfirst($r['action_type']) . ': "' . mb_substr($r['subject'], 0, 60) . '" from ' . ($r['from_name'] ?: $r['from_email']),
$rows
);
$reply = count($rows) . " email action item" . (count($rows)>1?'s':'') . " need attention, {$userAddr}: " . implode('. ', $items) . '.';
}
$source = 'email:action_items';
if ($emailCode === 200 && $emailJson) {
$ed = json_decode($emailJson, true) ?? [];
$summary = $ed['summary'] ?? [];
$unread = (int)($summary['total_unread'] ?? 0);
$recent = $summary['recent'] ?? [];
$accts = $ed['accounts'] ?? [];
// ── Create task from most recent action email ─────────────────────────────
} elseif (preg_match('/\b(create task|add task|make.*task|task.*from.*email)\b/i', $message)) {
$fromMatch = null;
if (preg_match('/\bfrom\s+([a-z][\w\s\.]+)/i', $message, $fm)) $fromMatch = trim($fm[1]);
$where = "WHERE dismissed=0 AND task_id IS NULL AND action_type IN ('task','appointment')";
$params = [];
if ($fromMatch) { $where .= ' AND (from_name LIKE ? OR from_email LIKE ?)'; $params[] = "%{$fromMatch}%"; $params[] = "%{$fromMatch}%"; }
$ea = JarvisDB::single("SELECT * FROM email_actions {$where} ORDER BY received_at DESC LIMIT 1", $params);
if ($ea) {
$title = mb_substr($ea['subject'], 0, 255);
$notes = "From: {$ea['from_name']} <{$ea['from_email']}>";
$taskId = JarvisDB::insert('INSERT INTO tasks (title,notes,category,priority) VALUES (?,?,?,?)',
[$title, $notes, 'work', 'normal']);
JarvisDB::execute('UPDATE email_actions SET task_id=?,dismissed=1 WHERE id=?', [$taskId, $ea['id']]);
$reply = "Task created from email: \"{$title}\", {$userAddr}.";
$source = 'email:create_task';
} else {
$reply = "No action-required emails found to create a task from, {$userAddr}.";
$source = 'email:create_task_none';
}
// "How many" / "any" / check → just count
if (preg_match('/\b(how many|any|check|count|unread)\b/i', $message) && !preg_match('/\bread\b/i', $message)) {
// ── Create appointment from meeting email ─────────────────────────────────
} elseif (preg_match('/\b(schedule|appointment|meeting|calendar).*\bemail\b|\bemail\b.*(schedule|appointment|meeting|calendar)\b/i', $message)) {
$ea = JarvisDB::single(
"SELECT * FROM email_actions WHERE dismissed=0 AND appointment_id IS NULL AND action_type='appointment' ORDER BY received_at DESC LIMIT 1"
);
if ($ea) {
$start = $ea['suggested_date'] ? $ea['suggested_date'] . ' 09:00:00' : date('Y-m-d') . ' 09:00:00';
$title = mb_substr($ea['subject'], 0, 255);
$apptId = JarvisDB::insert('INSERT INTO appointments (title,description,category,start_at) VALUES (?,?,?,?)',
[$title, "From: {$ea['from_name']}", 'work', $start]);
JarvisDB::execute('UPDATE email_actions SET appointment_id=?,dismissed=1 WHERE id=?', [$apptId, $ea['id']]);
$reply = "Appointment scheduled from email: \"{$title}\" on " . date('l, M j', strtotime($start)) . ", {$userAddr}.";
$source = 'email:create_appt';
} else {
$reply = "No meeting emails found to schedule, {$userAddr}.";
$source = 'email:create_appt_none';
}
// ── Unread count ──────────────────────────────────────────────────────────
} elseif (preg_match('/\b(how many|any|check|count|unread)\b/i', $message) && !preg_match('/\bread\b/i', $message)) {
$emailUrl = (defined('SITE_URL') ? SITE_URL : 'https://jarvis.orbishosting.com') . '/api/email';
$account = 'all';
if (preg_match('/\bgmail\b/i', $message)) $account = 'gmail';
if (preg_match('/\boutlook\b/i', $message)) $account = 'outlook';
if (preg_match('/\bicloud\b/i', $message)) $account = 'icloud';
$ch = curl_init($emailUrl . '?account=' . $account);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_HTTPHEADER=>['X-Session-Token: '.($_SESSION['jarvis_token']??'')], CURLOPT_TIMEOUT=>20, CURLOPT_CONNECTTIMEOUT=>5, CURLOPT_SSL_VERIFYPEER=>false]);
$emailJson = curl_exec($ch); $emailCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($emailCode === 200 && $emailJson) {
$ed = json_decode($emailJson, true) ?? [];
$unread = (int)($ed['summary']['total_unread'] ?? 0);
$aiCount = (int)($ed['action_items_count'] ?? 0);
$parts = [];
foreach ($ed['accounts'] ?? [] as $a => $r) {
if (!empty($r['unread'])) $parts[] = $r['unread'] . ' on ' . ucfirst($a);
}
if ($unread === 0) {
$reply = "No unread emails, {$userAddr}.";
} else {
// break down by account
$parts = [];
foreach ($accts as $a => $r) {
if (!empty($r['unread'])) $parts[] = $r['unread'] . ' on ' . ucfirst($a);
}
$breakdown = $parts ? ' (' . implode(', ', $parts) . ')' : '';
$reply = "You have {$unread} unread email" . ($unread>1?'s':'') . "{$breakdown}, {$userAddr}.";
$bd = $parts ? ' (' . implode(', ', $parts) . ')' : '';
$reply = "You have {$unread} unread email" . ($unread>1?'s':'') . "{$bd}, {$userAddr}.";
}
if ($aiCount > 0) $reply .= " {$aiCount} require action.";
$source = 'email:count';
}
// "emails from [person]" → filter by sender
} elseif (preg_match('/\bfrom\s+(.+)/i', $message, $fm)) {
$sender = strtolower(trim($fm[1]));
$matches = array_filter($recent, fn($m) =>
stripos($m['from_name']??'', $sender) !== false ||
stripos($m['from_email']??'', $sender) !== false
);
if ($matches) {
$m = array_values($matches)[0];
$reply = "Email from {$m['from_name']}: \"{$m['subject']}\"{$m['date']}.";
if (!empty($m['preview'])) $reply .= ' Preview: ' . mb_substr($m['preview'], 0, 150);
} else {
$reply = "No recent emails from {$sender}, {$userAddr}.";
// ── Read recent emails ────────────────────────────────────────────────────
} else {
$emailUrl = (defined('SITE_URL') ? SITE_URL : 'https://jarvis.orbishosting.com') . '/api/email';
$account = 'all';
if (preg_match('/\bgmail\b/i', $message)) $account = 'gmail';
if (preg_match('/\boutlook\b/i', $message)) $account = 'outlook';
if (preg_match('/\bicloud\b/i', $message)) $account = 'icloud';
$ch = curl_init($emailUrl . '?account=' . $account);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_HTTPHEADER=>['X-Session-Token: '.($_SESSION['jarvis_token']??'')], CURLOPT_TIMEOUT=>20, CURLOPT_CONNECTTIMEOUT=>5, CURLOPT_SSL_VERIFYPEER=>false]);
$emailJson = curl_exec($ch); $emailCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($emailCode === 200 && $emailJson) {
$ed = json_decode($emailJson, true) ?? [];
$recent = $ed['summary']['recent'] ?? [];
$unread = (int)($ed['summary']['total_unread'] ?? 0);
$aiCount = (int)($ed['action_items_count'] ?? 0);
// filter by sender if mentioned
if (preg_match('/\bfrom\s+(.+)/i', $message, $fm)) {
$sender = strtolower(trim($fm[1]));
$recent = array_values(array_filter($recent, fn($m) =>
stripos($m['from_name']??'', $sender)!==false || stripos($m['from_email']??'', $sender)!==false));
}
$source = 'email:search';
// "read" / "latest" / "recent" → read top emails
} else {
$unreadOnly = array_values(array_filter($recent, fn($m) => $m['unread']));
$toRead = $unreadOnly ?: $recent;
$toRead = array_slice($toRead, 0, 3);
$toRead = array_filter($recent, fn($m) => $m['unread']) ?: $recent;
$toRead = array_slice(array_values($toRead), 0, 3);
if (empty($toRead)) {
$reply = "No emails to report, {$userAddr}.";
} else {
$lines = [];
foreach ($toRead as $m) {
$flag = $m['unread'] ? '' : '';
$acct = isset($m['account']) ? ' [' . strtoupper($m['account']) . ']' : '';
$acct = isset($m['account']) ? ' [' . strtoupper($m['account']) . ']' : '';
$lines[] = "From {$m['from_name']}: \"{$m['subject']}\", {$m['date']}{$acct}.";
}
$intro = $unread > 0 ? "You have {$unread} unread. " : "";
$reply = $intro . implode(' ', $lines);
if ($aiCount > 0) $reply .= " {$aiCount} require action — say 'email action items' for details.";
}
$source = 'email:read';
}