mirror of
https://github.com/myronblair/jarvis
synced 2026-07-27 16:22:55 -05:00
Adopt live production state as git baseline
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* JARVIS Knowledge Base + Intent Engine
|
||||
* Matches user input against intent patterns, substitutes live facts from kb_facts.
|
||||
* Returns a response if matched, or null to escalate to Ollama/Claude.
|
||||
*/
|
||||
|
||||
class KBEngine {
|
||||
|
||||
/**
|
||||
* Try to match the input against known intents.
|
||||
* Returns ['reply' => 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user