string, 'intent' => string] or null if no match. */ public static function match(string $input): ?array { $intents = JarvisDB::query( 'SELECT * FROM kb_intents WHERE active=1 ORDER BY priority DESC, id ASC' ); if (!$intents) return null; foreach ($intents as $intent) { $pat = '~' . $intent['pattern'] . '~'; if (@preg_match($pat, $input)) { $reply = self::fillTemplate( $intent['response_template'], $intent['fact_category'] ); return [ 'reply' => $reply, 'intent' => $intent['intent_name'], 'action' => $intent['action_type'], ]; } } return null; } /** * Replace {placeholder} tokens in a template with values from kb_facts, * plus built-in dynamic tokens like {current_time}. */ private static function fillTemplate(string $template, ?string $category): string { // Built-in tokens $builtins = [ 'current_time' => date('g:i A'), 'current_date' => date('l, F j Y'), ]; // Load user address preference $prefRows = JarvisDB::query( "SELECT pref_key, pref_value FROM kb_preferences WHERE pref_key IN ('user_name','user_title')" ); $prefs = []; foreach ($prefRows ?? [] as $p) { $prefs[$p['pref_key']] = $p['pref_value']; } $builtins['user_title'] = $prefs['user_title'] ?? $prefs['user_name'] ?? 'Sir'; $builtins['user_name'] = $prefs['user_name'] ?? 'Myron'; // Computed builtins $pendingRow = JarvisDB::single("SELECT COUNT(*) cnt FROM tasks WHERE status NOT IN ('done','cancelled')"); $builtins['pending_count'] = (string)($pendingRow['cnt'] ?? 0); $overdueRow = JarvisDB::single("SELECT COUNT(*) cnt FROM tasks WHERE due_date < CURDATE() AND status NOT IN ('done','cancelled')"); $builtins['overdue_count'] = (string)($overdueRow['cnt'] ?? 0); // Fetch all facts for this category $facts = []; if ($category) { $rows = JarvisDB::query( 'SELECT fact_key, fact_value FROM kb_facts WHERE category = ? AND (expires_at IS NULL OR expires_at > NOW())', [$category] ); foreach ($rows ?? [] as $r) { $facts[$r['fact_key']] = $r['fact_value']; } } // Pull network facts if template uses them if (strpos($template, '{online_count}') !== false || strpos($template, '{total_count}') !== false) { $netRows = JarvisDB::query("SELECT fact_key, fact_value FROM kb_facts WHERE category='network' AND (expires_at IS NULL OR expires_at > NOW())"); foreach ($netRows ?? [] as $r) { $facts[$r['fact_key']] = $r['fact_value']; } } // Pull system facts if template uses them if (preg_match('/\{(cpu_usage|mem_|disk_|uptime|load_)\w*\}/', $template)) { $sysRows = JarvisDB::query("SELECT fact_key, fact_value FROM kb_facts WHERE category='system' AND (expires_at IS NULL OR expires_at > NOW())"); foreach ($sysRows ?? [] as $r) { $facts[$r['fact_key']] = $r['fact_value']; } } $allTokens = array_merge($builtins, $facts); // Replace placeholders return preg_replace_callback('/\{([a-z0-9_]+)\}/', function ($m) use ($allTokens) { return $allTokens[$m[1]] ?? ''; }, $template); } /** * Store a fact in kb_facts (upsert). */ public static function storeFact( string $category, string $key, string $value, string $host = 'local', ?int $ttlSeconds = null ): void { $expires = $ttlSeconds ? gmdate('Y-m-d H:i:s', time() + $ttlSeconds) : null; JarvisDB::execute( 'INSERT INTO kb_facts (category, fact_key, fact_value, host, expires_at) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), expires_at=VALUES(expires_at), updated_at=NOW()', [$category, $key, $value, $host, $expires] ); } /** * Learn from conversation — store interesting facts the user mentions. */ public static function learnFromConversation(string $input, string $reply): void { // Preference learning: user states a preference if (preg_match('/(?i)i (prefer|like|want|always)\s+(.+?)(?:\.|$)/', $input, $m)) { $pref = trim($m[2]); if (strlen($pref) < 120) { JarvisDB::execute( 'INSERT INTO kb_preferences (pref_key, pref_value) VALUES (?,?) ON DUPLICATE KEY UPDATE pref_value=VALUES(pref_value)', ['learned_' . md5($pref), $pref] ); } } } /** * Return a summary of what the KB knows (for system prompt injection). */ public static function getContextSummary(): string { // Exclude entity_map — too large for Ollama 1B tokenizer $facts = JarvisDB::query( "SELECT category, fact_key, fact_value FROM kb_facts WHERE fact_key != 'entity_map' ORDER BY category, updated_at DESC" ); if (!$facts) return ''; $byCategory = []; foreach ($facts as $f) { $byCategory[$f['category']][] = "{$f['fact_key']}={$f['fact_value']}"; } $lines = []; foreach ($byCategory as $cat => $items) { $lines[] = strtoupper($cat) . ': ' . implode(', ', array_slice($items, 0, 8)); } return implode("\n", $lines); } }