From 27a0259e645dd3f6a56acd364bbd4cfc3257d9d2 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Thu, 11 Jun 2026 12:33:05 +0000 Subject: [PATCH] =?UTF-8?q?Phase=2010:=20Memory=20Core=20=E2=80=94=20auto-?= =?UTF-8?q?extraction=20knowledge=20graph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reactor.py v9.0.0+: memory_extract + memory_store handlers (21 handlers) - handle_memory_extract: Haiku-powered fact extraction from conversations - handle_memory_store: explicit memory insertion from voice commands - FastAPI: /memory/facts CRUD, /memory/context (relevance retrieval), /memory/stats - chat.php: Tier 0.9k voice commands (remember/forget/recall/memory status) - Memory context injected into Groq + Claude system prompts - Auto-trigger memory_extract after every LLM response (async, non-blocking) - memory.php: new API endpoint proxying Arc Reactor memory routes - api.php: added memory route - admin/index.php: MEMORY CORE nav + tab (browse by category, search, add/delete facts) - index.html: MEMORY count in bottom bar (polls every 60s) --- api/endpoints/chat.php | 129 ++++++++++++++++++++- api/endpoints/memory.php | 81 +++++++++++++ public_html/admin/index.php | 223 ++++++++++++++++++++++++++++++++++++ public_html/api.php | 3 + public_html/index.html | 24 ++++ 5 files changed, 458 insertions(+), 2 deletions(-) create mode 100644 api/endpoints/memory.php 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)} + +
+
◈ MEMORY CORE — KNOWLEDGE GRAPH
+
+ + + + + +
+
+
LOADING MEMORY CORE...
+ + + +
+ @@ -1894,6 +1996,7 @@ function loadTab(tab) { missions: loadMissions, directives: loadDirectives, clearance: loadClearance, + memory: loadMemory, vision: loadVision, guardian: loadGuardian, tasks: loadTasks, @@ -3942,6 +4045,126 @@ async function syncCalNow() { }); } +// ── MEMORY CORE ─────────────────────────────────────────────────────────────── +const CAT_COLORS = { + preference:'var(--cyan)', person:'#a78bfa', place:'#00ff88', + routine:'#ffd700', goal:'#ff9900', fact:'var(--text)', instruction:'#ff6680' +}; + +async function loadMemory() { + const el = document.getElementById('memory-list'); + const cat = document.getElementById('mem-cat-filter').value; + const search = document.getElementById('mem-search').value; + el.innerHTML = '
LOADING...
'; + + const [facts, stats] = await Promise.all([ + api('memory_list' + (cat ? '&category='+encodeURIComponent(cat) : '') + (search ? '&search='+encodeURIComponent(search) : '') + '&limit=200'), + api('memory_stats'), + ]); + const list = Array.isArray(facts) ? facts : []; + const s = stats || {}; + + const bar = document.getElementById('memory-stats-bar'); + if (bar) { + const cats = (s.by_category||[]).map(c=>`${c.cnt} ${c.category}`).join(' · '); + bar.textContent = `${s.total||0} FACTS${cats ? ' — ' + cats : ''}`; + } + + if (!list.length) { + el.innerHTML = '
No memory facts yet. Start chatting with JARVIS — I auto-learn from conversations.
'; + return; + } + + // Group by category + const grouped = {}; + for (const f of list) { + if (!grouped[f.category]) grouped[f.category] = []; + grouped[f.category].push(f); + } + + let html = ''; + const catOrder = ['instruction','preference','person','place','routine','goal','fact']; + const allCats = [...new Set([...catOrder, ...Object.keys(grouped)])]; + for (const cat of allCats) { + if (!grouped[cat]) continue; + const color = CAT_COLORS[cat] || 'var(--text)'; + html += `
+
+ ${cat.toUpperCase()} (${grouped[cat].length}) + +
+ `; + for (const f of grouped[cat]) { + const conf = (parseFloat(f.confidence)*100).toFixed(0)+'%'; + const ts = f.last_confirmed_at ? new Date(f.last_confirmed_at).toLocaleDateString() : ''; + const srcColor = f.source === 'explicit' ? 'var(--green)' : f.source === 'inference' ? 'var(--yellow)' : 'var(--dim)'; + html += ` + + + + + + + + + `; + } + html += '
SUBJECTPREDICATEVALUECONFIDENCECONFIRMEDSOURCELAST SEEN
${esc(f.subject)}${esc(f.predicate)}${esc(f.object)}${conf}${f.confirmed_count}${f.source}${ts}
'; + } + el.innerHTML = html; +} + +function memoryNew() { + document.getElementById('memory-editor').style.display = 'block'; + document.getElementById('mem-new-subject').focus(); +} + +async function memorySave() { + const body = { + category: document.getElementById('mem-new-cat').value, + subject: document.getElementById('mem-new-subject').value.trim(), + predicate: document.getElementById('mem-new-predicate').value.trim() || 'is', + object: document.getElementById('mem-new-object').value.trim(), + }; + if (!body.subject || !body.object) { toast('Subject and value required','err'); return; } + try { + const r = await fetch(location.href + '?action=memory_store', { + method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) + }); + const d = await r.json(); + if (d.ok) { + toast('Fact stored', 'ok'); + document.getElementById('mem-new-subject').value = ''; + document.getElementById('mem-new-predicate').value = ''; + document.getElementById('mem-new-object').value = ''; + document.getElementById('memory-editor').style.display = 'none'; + loadMemory(); + } else { toast('Error: '+(d.detail||d.error||'unknown'),'err'); } + } catch(e) { toast('Failed','err'); } +} + +async function memoryDelete(id) { + if (!confirm('Delete this memory fact?')) return; + await fetch(location.href + '?action=memory_delete&id=' + id, {method:'POST'}); + toast('Deleted','ok'); + loadMemory(); +} + +async function memoryClearCategory(cat) { + if (!confirm('Clear all ' + cat + ' memories?')) return; + await fetch(location.href + '?action=memory_clear&category=' + encodeURIComponent(cat), {method:'POST'}); + toast('Cleared ' + cat + ' memories', 'ok'); + loadMemory(); +} + +async function memoryClearAll() { + if (!confirm('Clear ALL memory facts? This cannot be undone.')) return; + if (!confirm('Are you absolutely sure? All JARVIS memories will be deleted.')) return; + await fetch(location.href + '?action=memory_clear', {method:'POST'}); + toast('All memories cleared', 'ok'); + loadMemory(); +} + // ── CLEARANCE PROTOCOL ──────────────────────────────────────────────────────── async function loadClearance() { const [pending, rules, history] = await Promise.all([ diff --git a/public_html/api.php b/public_html/api.php index 5557fc8..0709239 100644 --- a/public_html/api.php +++ b/public_html/api.php @@ -105,6 +105,9 @@ switch ($endpoint) { case "directives": require __DIR__ . "/../api/endpoints/directives.php"; break; + case "memory": + require __DIR__ . "/../api/endpoints/memory.php"; + break; case "calendar": require __DIR__ . '/../api/endpoints/calendar_sync.php'; break; diff --git a/public_html/index.html b/public_html/index.html index e138d61..4311842 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -1322,6 +1322,11 @@ body::after{ +
+
+ MEMORY -- +
+
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED --:--:-- @@ -2508,6 +2513,11 @@ function showApp(name, greeting, silent = false) { updateClearanceBanner(); setInterval(updateClearanceBanner, 30000); }, 6000); + // Memory Core — poll count every 60s + setTimeout(() => { + updateMemoryCount(); + setInterval(updateMemoryCount, 60000); + }, 8000); } async function logout() { @@ -4408,6 +4418,20 @@ async function hudDirectiveReview(id) { } } +// ── MEMORY CORE — bottom bar count ──────────────────────────────────────────── +async function updateMemoryCount() { + try { + const stats = await api('memory?action=stats'); + const el = document.getElementById('bb-memory-count'); + const dot = document.getElementById('bb-memory-dot'); + if (el && stats) { + const total = stats.total || 0; + el.textContent = total + ' FACTS'; + if (dot) dot.style.background = total > 0 ? 'var(--cyan)' : 'rgba(0,212,255,0.3)'; + } + } catch(e) {} +} + // ── CLEARANCE PROTOCOL HUD ───────────────────────────────────────────────────── const _clrOpenCards = new Set();