Files
jarvis/api/lib/kb_engine.php
T
myron dc55e6c45b Initial commit: JARVIS AI dashboard v2.3
- 4-tier chat: HA control → Ollama → Groq → Claude
- Push-based agent system with heartbeat/metrics
- Network monitoring, alerts, Proxmox, Home Assistant
- Windows + Linux agent installers
- Stats cache cron, facts collector, KB engine
2026-05-25 13:22:57 +00:00

140 lines
4.8 KiB
PHP

<?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'),
];
// 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);
}
}