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:
2026-06-11 12:33:05 +00:00
parent 93d7594c4f
commit 27a0259e64
5 changed files with 458 additions and 2 deletions
+223
View File
@@ -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([