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
+127 -2
View File
@@ -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,
+81
View File
@@ -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]);
}