diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index 7babf1b..75d133d 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -1081,6 +1081,52 @@ if (!$reply && preg_match('/\b(news|headlines|latest|what.?s happening|current e // ── Tier 0.9: Arc Protocols — research, triage, remote_exec, screenshot, sysinfo ─ $arcJobId = null; +// ── Memory Core helpers ─────────────────────────────────────────────────────── + +function getMemoryContext(string $message, int $limit = 12): string { + try { + $ch = curl_init('http://127.0.0.1:7474/memory/context?limit=' . $limit . + '&message=' . urlencode(substr($message, 0, 200))); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 2, + CURLOPT_CONNECTTIMEOUT => 1, + ]); + $raw = curl_exec($ch); + curl_close($ch); + if (!$raw) return ''; + $data = json_decode($raw, true); + return $data['context'] ?? ''; + } catch (Throwable $e) { + return ''; + } +} + +function memoryExtractAsync(string $userMsg, string $assistantMsg, string $sessionId): void { + // Fire-and-forget background memory extraction — does not block the response + $body = json_encode([ + 'type' => 'memory_extract', + 'payload' => [ + 'user_message' => substr($userMsg, 0, 600), + 'assistant_message' => substr($assistantMsg, 0, 600), + 'conversation_id' => null, + ], + 'priority' => 2, + 'created_by' => 'chat:' . $sessionId, + ]); + $ch = curl_init('http://127.0.0.1:7474/job'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_TIMEOUT => 1, + CURLOPT_CONNECTTIMEOUT => 1, + ]); + @curl_exec($ch); + curl_close($ch); +} + // Helper: submit job to Arc Reactor function arcPost(string $path, array $body): ?array { $ch = curl_init('http://127.0.0.1:7474' . $path); @@ -1481,6 +1527,71 @@ if (!$reply) { } } +// ── Tier 0.9k: Memory Core — remember/forget/recall voice commands ─────────── +if (!$reply) { + // "remember that I prefer X", "remember X", "note that X" + if (preg_match('/^(?:jarvis[,\s]+)?(?:remember|note down|make a note|remember that|note that)\s+(?:that\s+)?(.+)/i', $message, $m)) { + $fact = trim($m[1]); + // Use LLM to parse the fact into subject/predicate/object — but for speed, use heuristic + $arcRes = arcPost('/job', [ + 'type' => 'memory_store', + 'payload' => ['subject' => 'user', 'predicate' => 'note', 'object' => $fact, 'category' => 'instruction'], + 'priority' => 8, + 'created_by' => 'chat:' . $sessionId, + ]); + $reply = "Noted, {$userAddr}. I'll remember that: {$fact}"; + $source = 'memory:store'; + } +} +if (!$reply) { + // "forget X", "forget that X", "remove memory X" + if (preg_match('/^(?:jarvis[,\s]+)?(?:forget|discard|remove|delete)\s+(?:that\s+|the\s+memory\s+(?:about\s+)?)?(.+)/i', $message, $m)) { + $keyword = trim($m[1]); + // Mark matching facts inactive + $affected = JarvisDB::execute( + "UPDATE memory_facts SET active=0 WHERE active=1 AND (subject LIKE ? OR object LIKE ? OR predicate LIKE ?)", + ["%{$keyword}%", "%{$keyword}%", "%{$keyword}%"] + ); + $reply = $affected + ? "Memory cleared, {$userAddr}. Removed facts related to: {$keyword}." + : "Nothing in memory matched \"{$keyword}\", {$userAddr}."; + $source = 'memory:forget'; + } +} +if (!$reply) { + // "what do you know about X", "what do you remember about X" + if (preg_match('/^(?:jarvis[,\s]+)?(?:what do you know about|what do you remember about|recall|memory about)\s+(.+)/i', $message, $m)) { + $keyword = trim($m[1]); + $facts = JarvisDB::query( + "SELECT * FROM memory_facts WHERE active=1 AND (subject LIKE ? OR object LIKE ?) ORDER BY confirmed_count DESC LIMIT 8", + ["%{$keyword}%", "%{$keyword}%"] + ) ?: []; + if (empty($facts)) { + $reply = "I don't have any stored memories about \"{$keyword}\", {$userAddr}."; + } else { + $lines = array_map(fn($f) => "{$f['subject']} {$f['predicate']}: {$f['object']}", $facts); + $reply = "◈ I know the following about \"{$keyword}\", {$userAddr}:\n" . implode("\n", $lines); + } + $source = 'memory:recall'; + } +} +if (!$reply) { + // "how many memories", "memory status", "what do you know about me" + if (preg_match('/^(?:jarvis[,\s]+)?(?:(?:how many|show)\s+memories|memory\s+(?:status|count|summary)|what do you know about me)/i', $message)) { + $stats = JarvisDB::query( + "SELECT category, COUNT(*) cnt FROM memory_facts WHERE active=1 GROUP BY category ORDER BY cnt DESC" + ) ?: []; + $total = array_sum(array_column($stats, 'cnt')); + if (!$total) { + $reply = "My memory core is empty, {$userAddr}. I'll start learning from our conversations."; + } else { + $breakdown = implode(', ', array_map(fn($s) => "{$s['cnt']} {$s['category']}", $stats)); + $reply = "◈ Memory Core: {$total} facts stored — {$breakdown}. I use these to personalize responses."; + } + $source = 'memory:status'; + } +} + // ── Tier 1: Intent Engine (instant, no LLM) ─────────────────────────────── if (!$reply) { $matched = KBEngine::match($message); @@ -1612,6 +1723,12 @@ if (!$reply) { } +// ── Memory injection — fetch relevant facts before LLM tiers ───────────── +$memoryContext = ''; +if (!$reply) { + $memoryContext = getMemoryContext($message, 12); +} + // ── Tier 2: Ollama local LLM (fast local fallback) ─────────────────────── if (!$reply && defined('OLLAMA_HOST') && OLLAMA_HOST) { $ollamaHost = OLLAMA_HOST; @@ -1672,10 +1789,12 @@ if (!$reply && defined('GROQ_API_KEY') && GROQ_API_KEY) { ); $groqModel = $needsSearch ? GROQ_MODEL_SEARCH : GROQ_MODEL_GENERAL; + $memSuffix = $memoryContext ? "\n\n{$memoryContext}" : ''; $groqMessages = [['role' => 'system', 'content' => "You are JARVIS — Just A Rather Very Intelligent System — the AI of {$userName} " . "(address him as \"{$userAddr}\"). Formal, efficient, British butler tone. " . - 'Be concise — 2-4 sentences unless detail is explicitly requested. Today: ' . date('D M j Y g:i A T') . '.'], + 'Be concise — 2-4 sentences unless detail is explicitly requested. Today: ' . date('D M j Y g:i A T') . + $memSuffix . '.'], ]; foreach (array_slice($history, -6) as $h) { $groqMessages[] = ['role' => $h['role'], 'content' => $h['content']]; @@ -1757,7 +1876,8 @@ Infrastructure: - Network: 10.48.200.0/24, FortiGate firewall Live data: -{$systemContext}" . ($kbContext ? "\nKnowledge base:\n{$kbContext}" : '') . " +{$systemContext}" . ($kbContext ? "\nKnowledge base:\n{$kbContext}" : '') . +($memoryContext ? "\n\n{$memoryContext}" : '') . " Today: " . date('l, F j Y, g:i A T') . " Respond as JARVIS. Voice readout: under 3 sentences unless detail is requested. For system status, interpret the data and give an assessment — don't just recite numbers."; @@ -1827,6 +1947,11 @@ JarvisDB::insert( ); KBEngine::learnFromConversation($message, $reply); +// Memory Core — async extraction for LLM responses (don't extract from intent/KB/fallback) +if ($reply && !in_array(explode(':', $source)[0], ['intent', 'kb', 'fallback', 'memory', 'arc'])) { + memoryExtractAsync($message, $reply, $sessionId); +} + echo json_encode([ 'reply' => $reply, 'source' => $source, diff --git a/api/endpoints/memory.php b/api/endpoints/memory.php new file mode 100644 index 0000000..614d6c9 --- /dev/null +++ b/api/endpoints/memory.php @@ -0,0 +1,81 @@ +true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + return json_decode($raw, true) ?: []; +} + +function memArcPost(string $path, array $body): array { + $ch = curl_init('http://127.0.0.1:7474' . $path); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>5, + CURLOPT_POSTFIELDS => json_encode($body), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + return json_decode($raw, true) ?: []; +} + +function memArcDelete(string $path): array { + $ch = curl_init('http://127.0.0.1:7474' . $path); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_CUSTOMREQUEST=>'DELETE', CURLOPT_TIMEOUT=>5, + ]); + $raw = curl_exec($ch); curl_close($ch); + return json_decode($raw, true) ?: ['ok' => true]; +} + +switch ($action) { + case 'list': + $limit = min((int)($_GET['limit'] ?? 100), 500); + $category = $_GET['category'] ?? ''; + $search = $_GET['search'] ?? ''; + $qs = http_build_query(array_filter(['limit'=>$limit,'category'=>$category,'search'=>$search])); + echo json_encode(memArcGet('/memory/facts' . ($qs ? '?'.$qs : ''))); + break; + + case 'stats': + echo json_encode(memArcGet('/memory/stats')); + break; + + case 'context': + $msg = $_GET['message'] ?? ''; + $limit = (int)($_GET['limit'] ?? 12); + $qs = http_build_query(['message'=>$msg,'limit'=>$limit]); + echo json_encode(memArcGet('/memory/context?' . $qs)); + break; + + case 'store': + $subject = trim($data['subject'] ?? ''); + $predicate = trim($data['predicate'] ?? 'is'); + $object = trim($data['object'] ?? ''); + $category = $data['category'] ?? 'fact'; + if (!$subject || !$object) { http_response_code(400); echo json_encode(['error'=>'subject and object required']); break; } + echo json_encode(memArcPost('/memory/facts', ['subject'=>$subject,'predicate'=>$predicate,'object'=>$object,'category'=>$category])); + break; + + case 'delete': + $id = (int)($_GET['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error'=>'Missing id']); break; } + echo json_encode(memArcDelete('/memory/facts/' . $id)); + break; + + case 'clear': + $category = $_GET['category'] ?? ''; + $qs = $category ? '?category=' . urlencode($category) : ''; + echo json_encode(memArcDelete('/memory/facts' . $qs)); + break; + + default: + http_response_code(404); + echo json_encode(['error' => 'Unknown memory action: ' . $action]); +} diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 9413b73..7e0de13 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -833,6 +833,49 @@ if ($action) { $raw = curl_exec($ch); curl_close($ch); j(json_decode($raw, true) ?: ['ok'=>true]); + // ── MEMORY CORE ────────────────────────────────────────────────────── + case 'memory_list': + $limit = min((int)($_GET['limit'] ?? 200), 500); + $category = $_GET['category'] ?? ''; + $search = $_GET['search'] ?? ''; + $qs = http_build_query(array_filter(['limit'=>$limit,'category'=>$category,'search'=>$search])); + $ch = curl_init('http://127.0.0.1:7474/memory/facts' . ($qs ? '?'.$qs : '')); + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw,true) ?: []); + + case 'memory_stats': + $ch = curl_init('http://127.0.0.1:7474/memory/stats'); + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw,true) ?: ['total'=>0,'by_category'=>[]]); + + case 'memory_store': + $body = json_decode(file_get_contents('php://input'),true) ?: []; + $ch = curl_init('http://127.0.0.1:7474/memory/facts'); + curl_setopt_array($ch,[ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>5, + CURLOPT_POSTFIELDS => json_encode($body), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw,true) ?: ['ok'=>true]); + + case 'memory_delete': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $ch = curl_init('http://127.0.0.1:7474/memory/facts/' . $id); + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_CUSTOMREQUEST=>'DELETE',CURLOPT_TIMEOUT=>5]); + curl_exec($ch); curl_close($ch); + j(['ok'=>true]); + + case 'memory_clear': + $category = $_GET['category'] ?? ''; + $url = 'http://127.0.0.1:7474/memory/facts' . ($category ? '?category=' . urlencode($category) : ''); + $ch = curl_init($url); + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_CUSTOMREQUEST=>'DELETE',CURLOPT_TIMEOUT=>5]); + curl_exec($ch); curl_close($ch); + j(['ok'=>true]); + // ── VISION PROTOCOL ────────────────────────────────────────────────── case 'vision_list': $limit = min((int)($_GET['limit'] ?? 30), 100); @@ -1171,6 +1214,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
+ @@ -1778,6 +1822,64 @@ select.filter-sel:focus{border-color:var(--cyan)} + +| SUBJECT | PREDICATE | VALUE | CONFIDENCE | CONFIRMED | SOURCE | LAST SEEN | |
|---|---|---|---|---|---|---|---|
| ${esc(f.subject)} | +${esc(f.predicate)} | +${esc(f.object)} | +${conf} | +${f.confirmed_count} | +${f.source} | +${ts} | ++ |