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'), ]; // Fetch all facts for this category (and null-category universal facts) $facts = []; if ($category) { $rows = JarvisDB::query( 'SELECT fact_key, fact_value FROM kb_facts WHERE category = ?', [$category] ); foreach ($rows as $r) { $facts[$r['fact_key']] = $r['fact_value']; } } // Also pull network facts for network tokens used in any template 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'" ); foreach ($netRows 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]] ?? '[unknown]'; }, $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)', [$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); } }