mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Phase 10: Memory Core — auto-extraction knowledge graph
- 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)
This commit is contained in:
+127
-2
@@ -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,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* JARVIS Memory Core API
|
||||
*/
|
||||
require_once __DIR__ . '/../../api/lib/db.php';
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
function memArcGet(string $path): array {
|
||||
$ch = curl_init('http://127.0.0.1:7474' . $path);
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>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]);
|
||||
}
|
||||
@@ -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)}
|
||||
<div class="nav-item" data-tab="missions" onclick="nav(this)">◈ MISSION OPS</div>
|
||||
<div class="nav-item" data-tab="directives" onclick="nav(this)">◈ DIRECTIVES</div>
|
||||
<div class="nav-item" data-tab="clearance" onclick="nav(this)" id="nav-clearance">🔒 CLEARANCE</div>
|
||||
<div class="nav-item" data-tab="memory" onclick="nav(this)" id="nav-memory">◈ MEMORY CORE</div>
|
||||
<div class="nav-section">INFO</div>
|
||||
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</div>
|
||||
<div class="nav-item" data-tab="users" onclick="nav(this)">USERS</div>
|
||||
@@ -1778,6 +1822,64 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MEMORY CORE -->
|
||||
<div class="tab" id="tab-memory">
|
||||
<div class="page-title">◈ MEMORY CORE — KNOWLEDGE GRAPH</div>
|
||||
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
|
||||
<button class="btn btn-sm btn-green" onclick="memoryNew()">+ ADD FACT</button>
|
||||
<button class="btn btn-sm" onclick="loadMemory()">↻ REFRESH</button>
|
||||
<select id="mem-cat-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadMemory()">
|
||||
<option value="">ALL CATEGORIES</option>
|
||||
<option value="preference">PREFERENCE</option>
|
||||
<option value="person">PERSON</option>
|
||||
<option value="place">PLACE</option>
|
||||
<option value="routine">ROUTINE</option>
|
||||
<option value="goal">GOAL</option>
|
||||
<option value="fact">FACT</option>
|
||||
<option value="instruction">INSTRUCTION</option>
|
||||
</select>
|
||||
<input id="mem-search" class="inp" placeholder="Search..." style="width:160px;padding:4px 8px;font-size:0.65rem" oninput="loadMemory()">
|
||||
<button class="btn btn-sm btn-red" onclick="memoryClearAll()">CLEAR ALL</button>
|
||||
<div id="memory-stats-bar" style="margin-left:auto;font-family:var(--mono);font-size:0.65rem;color:var(--dim)"></div>
|
||||
</div>
|
||||
<div id="memory-list"><div class="loading">LOADING MEMORY CORE...</div></div>
|
||||
|
||||
<!-- Add fact panel -->
|
||||
<div id="memory-editor" style="display:none;margin-top:20px;border:1px solid var(--border);border-radius:6px;padding:16px;background:rgba(0,212,255,0.02)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<div style="font-family:var(--mono);font-size:0.7rem;letter-spacing:2px;color:var(--cyan)">◈ ADD MEMORY FACT</div>
|
||||
<button class="btn btn-xs" onclick="document.getElementById('memory-editor').style.display='none'">✕</button>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px;margin-bottom:10px">
|
||||
<div>
|
||||
<div class="lbl">CATEGORY</div>
|
||||
<select id="mem-new-cat" class="inp">
|
||||
<option value="fact">FACT</option>
|
||||
<option value="preference">PREFERENCE</option>
|
||||
<option value="person">PERSON</option>
|
||||
<option value="place">PLACE</option>
|
||||
<option value="routine">ROUTINE</option>
|
||||
<option value="goal">GOAL</option>
|
||||
<option value="instruction">INSTRUCTION</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div class="lbl">SUBJECT</div>
|
||||
<input id="mem-new-subject" class="inp" placeholder="e.g. user, Tom">
|
||||
</div>
|
||||
<div>
|
||||
<div class="lbl">PREDICATE</div>
|
||||
<input id="mem-new-predicate" class="inp" placeholder="e.g. prefers, works at">
|
||||
</div>
|
||||
<div>
|
||||
<div class="lbl">VALUE</div>
|
||||
<input id="mem-new-object" class="inp" placeholder="e.g. dark mode">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-green" onclick="memorySave()">◈ SAVE FACT</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /content -->
|
||||
</div><!-- /main -->
|
||||
</div><!-- /app -->
|
||||
@@ -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 = '<div class="loading">LOADING...</div>';
|
||||
|
||||
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 = '<div class="empty-state">No memory facts yet. Start chatting with JARVIS — I auto-learn from conversations.</div>';
|
||||
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 += `<div style="margin-bottom:16px">
|
||||
<div style="font-family:var(--mono);font-size:0.65rem;letter-spacing:2px;color:${color};margin-bottom:6px;display:flex;align-items:center;gap:8px">
|
||||
${cat.toUpperCase()} <span style="color:var(--dim)">(${grouped[cat].length})</span>
|
||||
<button class="btn btn-xs btn-red" onclick="memoryClearCategory('${cat}')" style="margin-left:auto">CLEAR</button>
|
||||
</div>
|
||||
<table class="tbl"><thead><tr><th>SUBJECT</th><th>PREDICATE</th><th>VALUE</th><th>CONFIDENCE</th><th>CONFIRMED</th><th>SOURCE</th><th>LAST SEEN</th><th></th></tr></thead><tbody>`;
|
||||
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 += `<tr>
|
||||
<td style="font-family:var(--mono);font-size:0.65rem;color:${color}">${esc(f.subject)}</td>
|
||||
<td style="font-family:var(--mono);font-size:0.6rem;color:var(--dim)">${esc(f.predicate)}</td>
|
||||
<td style="font-family:var(--mono);font-size:0.6rem">${esc(f.object)}</td>
|
||||
<td style="font-family:var(--mono);font-size:0.6rem">${conf}</td>
|
||||
<td style="font-family:var(--mono);font-size:0.6rem;text-align:center">${f.confirmed_count}</td>
|
||||
<td style="font-family:var(--mono);font-size:0.55rem;color:${srcColor}">${f.source}</td>
|
||||
<td style="font-family:var(--mono);font-size:0.55rem;color:var(--dim)">${ts}</td>
|
||||
<td><button class="btn btn-xs btn-red" onclick="memoryDelete(${f.id})">DEL</button></td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
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([
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1322,6 +1322,11 @@ body::after{
|
||||
<span id="bb-guardian-badge" style="display:none;background:var(--red);color:#fff;font-size:0.45rem;padding:1px 4px;border-radius:2px;font-family:var(--font-mono);letter-spacing:0">0</span>
|
||||
</div>
|
||||
|
||||
<div class="bb-item" style="cursor:pointer" onclick="switchTab('clearance')" id="bb-memory-item">
|
||||
<div class="bb-dot" id="bb-memory-dot" style="background:rgba(0,212,255,0.3)"></div>
|
||||
<span>MEMORY</span> <span id="bb-memory-count" style="color:var(--text-dim)">--</span>
|
||||
</div>
|
||||
|
||||
<div id="ekgWrap" style="margin-left:auto"><canvas id="ekgCanvas"></canvas></div>
|
||||
<div style="font-size:0.65rem;flex-shrink:0">
|
||||
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span>
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user