mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: Phase 2 — Intel Protocol + Iron Protocol
Arc Reactor v2.0: - research handler: DDG search → async page fetch → trafilatura extraction → Claude synthesis - tool_loop handler: multi-step agent loop (up to 200 iter) with web_search, fetch_url, jarvis_agents, jarvis_alerts, current_time tools - llm handler: multi-provider router (Claude/Groq/Ollama) - /jobs/recent endpoint for HUD polling - Phase 1 handlers preserved (ping/echo/shell) chat.php — Tier 0.9 Intel Protocol (before KB intent engine): - Detects: research/investigate/deep-dive/look up/find out about → research job - Detects: step-by-step/figure out/analyze and report → tool_loop job - Returns arc_job ID in response for UI polling - Depth modifiers: quick/standard/deep index.html: - INTEL tab in right panel tab bar - Research result cards with expand/collapse, synthesis, sources, status - Live polling (4s) when INTEL tab is active + active jobs present - Auto-switches to INTEL tab when research is triggered from chat - intelPrompt() pre-fills chat input for new research Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1063,6 +1063,67 @@ if (!$reply && preg_match('/\b(news|headlines|latest|what.?s happening|current e
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tier 0.9: Intel Protocol — research & tool_loop detection ────────────
|
||||
$arcJobId = null;
|
||||
|
||||
// Detect "research X", "look up X", "deep dive X", "investigate X", "find out about X"
|
||||
$intelPatterns = [
|
||||
'/^(?:jarvis[,\s]+)?(?:research|investigate|deep[- ]dive|deep dive)\s+(.+)/i' => 'research',
|
||||
'/^(?:jarvis[,\s]+)?(?:look\s+(?:up|into)|find\s+out\s+(?:about)?)\s+(.+)/i' => 'research',
|
||||
'/^(?:jarvis[,\s]+)?(?:step[- ]by[- ]step|figure\s+out|analyze\s+and\s+report|work\s+through)\s+(.+)/i' => 'tool_loop',
|
||||
'/^(?:jarvis[,\s]+)?(?:run\s+a\s+research\s+(?:job|task)\s+on)\s+(.+)/i' => 'research',
|
||||
];
|
||||
|
||||
if (!$reply) {
|
||||
foreach ($intelPatterns as $pattern => $jobType) {
|
||||
if (preg_match($pattern, $message, $m)) {
|
||||
$queryOrTask = trim($m[1]);
|
||||
if (strlen($queryOrTask) < 3) break;
|
||||
|
||||
$depth = 'standard';
|
||||
if (preg_match('/\b(?:quick|brief)\b/i', $message)) $depth = 'quick';
|
||||
if (preg_match('/\b(?:deep|thorough|comprehensive|full)\b/i', $message)) $depth = 'deep';
|
||||
|
||||
$jobPayload = $jobType === 'research'
|
||||
? ['query' => $queryOrTask, 'depth' => $depth, 'provider' => 'claude']
|
||||
: ['task' => $queryOrTask, 'max_iterations' => 12, 'provider' => 'claude'];
|
||||
|
||||
// Submit to Arc Reactor
|
||||
$arcCh = curl_init('http://127.0.0.1:7474/job');
|
||||
curl_setopt_array($arcCh, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode([
|
||||
'type' => $jobType,
|
||||
'payload' => $jobPayload,
|
||||
'priority' => 7,
|
||||
'created_by' => 'chat:' . $sessionId,
|
||||
]),
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_CONNECTTIMEOUT => 3,
|
||||
]);
|
||||
$arcRes = json_decode(curl_exec($arcCh), true);
|
||||
curl_close($arcCh);
|
||||
|
||||
if (isset($arcRes['job_id'])) {
|
||||
$arcJobId = $arcRes['job_id'];
|
||||
if ($jobType === 'research') {
|
||||
$depthLabel = strtoupper($depth);
|
||||
$reply = "◈ INTEL PROTOCOL ACTIVATED — Running {$depthLabel} research on **{$queryOrTask}** (Job #{$arcJobId}). I'm searching sources, extracting content, and synthesizing a briefing now, {$userAddr}. Switch to the INTEL tab to watch live progress.";
|
||||
} else {
|
||||
$reply = "◈ IRON PROTOCOL ACTIVATED — Multi-step analysis initiated for **{$queryOrTask}** (Job #{$arcJobId}). I'll work through this systematically using available tools, {$userAddr}. Results will appear in the INTEL tab.";
|
||||
}
|
||||
$source = "arc:{$jobType}";
|
||||
} else {
|
||||
$reply = "Intel Protocol is offline, {$userAddr}. Arc Reactor may be unavailable — I'll try to answer directly.";
|
||||
$source = 'arc:offline';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tier 1: Intent Engine (instant, no LLM) ───────────────────────────────
|
||||
if (!$reply) {
|
||||
$matched = KBEngine::match($message);
|
||||
@@ -1193,6 +1254,7 @@ if (!$reply) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Tier 2: Ollama local LLM (fast local fallback) ───────────────────────
|
||||
if (!$reply && defined('OLLAMA_HOST') && OLLAMA_HOST) {
|
||||
$ollamaHost = OLLAMA_HOST;
|
||||
@@ -1413,4 +1475,5 @@ echo json_encode([
|
||||
'source' => $source,
|
||||
'session_id' => $sessionId,
|
||||
'timestamp' => date('c'),
|
||||
'arc_job' => $arcJobId,
|
||||
]);
|
||||
|
||||
@@ -875,6 +875,27 @@ body::after{
|
||||
transition:filter 1.2s ease;
|
||||
}
|
||||
#app.sleeping #sleepOverlay{display:flex}
|
||||
/* ── INTEL PROTOCOL — research result cards ──────────────────────── */
|
||||
.intel-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:8px;overflow:hidden}
|
||||
.intel-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none}
|
||||
.intel-card-head:hover{background:rgba(0,212,255,0.06)}
|
||||
.intel-card-query{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.intel-card-status{font-family:var(--font-mono);font-size:0.55rem;padding:2px 6px;border-radius:2px;flex-shrink:0}
|
||||
.intel-card-status.running{color:#ffd700;border:1px solid rgba(255,215,0,0.4);animation:pulse 1.5s ease-in-out infinite}
|
||||
.intel-card-status.done{color:var(--green);border:1px solid rgba(0,255,136,0.3)}
|
||||
.intel-card-status.failed{color:var(--red);border:1px solid rgba(255,34,68,0.3)}
|
||||
.intel-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)}
|
||||
.intel-card.open .intel-card-body{display:block}
|
||||
.intel-card-body .synthesis{font-size:0.65rem;line-height:1.6;color:var(--text);margin:8px 0;white-space:pre-wrap}
|
||||
.intel-sources{margin-top:8px}
|
||||
.intel-source{font-size:0.58rem;color:var(--text-dim);padding:2px 0;border-bottom:1px solid rgba(0,212,255,0.06)}
|
||||
.intel-source a{color:var(--cyan2);text-decoration:none}
|
||||
.intel-source a:hover{text-decoration:underline}
|
||||
.intel-empty{text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px}
|
||||
.intel-new-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;cursor:pointer;margin-bottom:8px}
|
||||
.intel-new-btn:hover{background:rgba(0,212,255,0.12)}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1077,6 +1098,7 @@ body::after{
|
||||
<div class="tab" onclick="switchTab('news')">NEWS</div>
|
||||
<div class="tab" onclick="switchTab('agents')">AGENTS</div>
|
||||
<div class="tab" onclick="switchTab('sites')">SITES</div>
|
||||
<div class="tab" id="tab-btn-intel" onclick="switchTab('intel')">INTEL</div>
|
||||
</div>
|
||||
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||
<div id="vm-list"><div class="loading-shimmer"></div></div>
|
||||
@@ -1093,6 +1115,9 @@ body::after{
|
||||
<div id="tab-agents" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||
<div id="agents-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
<div id="tab-intel" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
||||
<div id="intel-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -2997,6 +3022,7 @@ function switchTab(name) {
|
||||
if (pane) pane.classList.add('active');
|
||||
if (name === 'news') loadNews();
|
||||
if (name === 'agents') loadAgents();
|
||||
if (name === 'intel') loadIntel();
|
||||
if (name === 'alerts') loadAlerts();
|
||||
}
|
||||
|
||||
@@ -3140,6 +3166,7 @@ async function sendMessage() {
|
||||
addMessage('jarvis', data.reply);
|
||||
speak(data.reply);
|
||||
}
|
||||
if (data.arc_job) { onArcJobStarted(data.arc_job, data.source || ''); }
|
||||
} catch(e) {
|
||||
const bubble = document.getElementById('thinking-bubble');
|
||||
if (bubble) bubble.remove();
|
||||
@@ -3497,6 +3524,135 @@ async function arcWaitJob(jobId, onProgress) {
|
||||
}
|
||||
|
||||
|
||||
// ── INTEL PROTOCOL — HUD panel ────────────────────────────────────────
|
||||
let _intelPollTimer = null;
|
||||
let _intelActiveJobs = new Set();
|
||||
let _intelLastLoad = 0;
|
||||
|
||||
async function loadIntel() {
|
||||
const el = document.getElementById('intel-list');
|
||||
if (!el) return;
|
||||
_intelLastLoad = Date.now();
|
||||
|
||||
try {
|
||||
// Fetch recent research + tool_loop jobs
|
||||
const [resJobs, toolJobs] = await Promise.all([
|
||||
api('arc?action=jobs&status=&limit=20').catch(() => []),
|
||||
Promise.resolve([]),
|
||||
]);
|
||||
const jobs = Array.isArray(resJobs) ? resJobs.filter(j => ['research','tool_loop','llm'].includes(j.job_type)) : [];
|
||||
|
||||
if (!jobs.length) {
|
||||
el.innerHTML = '<div class="intel-empty">◈ NO INTEL JOBS<br><span style="opacity:0.5">Say "research [topic]" to activate</span></div>';
|
||||
stopIntelPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for active jobs
|
||||
const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'running');
|
||||
if (hasActive) startIntelPolling(); else stopIntelPolling();
|
||||
|
||||
let html = '<button class="intel-new-btn" onclick="intelPrompt()">⚡ NEW RESEARCH</button>';
|
||||
for (const job of jobs) {
|
||||
const isOpen = _intelActiveJobs.has(job.id) || job.status === 'running';
|
||||
const statusClass = job.status === 'done' ? 'done' : job.status === 'failed' ? 'failed' : 'running';
|
||||
const statusLabel = job.status === 'queued' ? 'QUEUED' : job.status === 'running' ? '● ACTIVE' : job.status.toUpperCase();
|
||||
const typeLabel = job.job_type === 'research' ? '◈ INTEL' : job.job_type === 'tool_loop' ? '⚡ IRON' : '◈ LLM';
|
||||
|
||||
// Get result details if done
|
||||
let bodyHtml = '';
|
||||
if (job.status === 'done' && job.result) {
|
||||
let r = job.result;
|
||||
if (typeof r === 'string') { try { r = JSON.parse(r); } catch(e) {} }
|
||||
if (typeof r === 'object') {
|
||||
const synthesis = (r.synthesis || r.result || r.response || '').trim();
|
||||
const sources = r.sources || [];
|
||||
const query = r.query || r.task || '';
|
||||
const provider = r.provider || '';
|
||||
|
||||
bodyHtml = `<div class="intel-card-body">`;
|
||||
if (provider) bodyHtml += `<div style="font-size:0.55rem;color:var(--text-dim);margin:6px 0 2px;font-family:var(--font-mono)">PROVIDER: ${provider.toUpperCase()} · SOURCES: ${r.source_count||sources.length||'—'}</div>`;
|
||||
if (synthesis) bodyHtml += `<div class="synthesis">${escHtml(synthesis.substring(0, 1500))}${synthesis.length>1500?'
|
||||
|
||||
[...truncated — view in admin]':''}</div>`;
|
||||
if (sources.length) {
|
||||
bodyHtml += '<div class="intel-sources"><div style="font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin-bottom:4px;font-family:var(--font-display)">SOURCES</div>';
|
||||
sources.slice(0,5).forEach((s,i) => {
|
||||
const title = escHtml((s.title||s.url||'').substring(0,60));
|
||||
const url = escHtml(s.url||'');
|
||||
bodyHtml += `<div class="intel-source">${i+1}. <a href="${url}" target="_blank" rel="noopener">${title||url}</a></div>`;
|
||||
});
|
||||
bodyHtml += '</div>';
|
||||
}
|
||||
bodyHtml += '</div>';
|
||||
}
|
||||
} else if (job.status === 'running' || job.status === 'queued') {
|
||||
const typeMsg = job.job_type === 'research' ? 'Searching sources and extracting content...' : 'Executing tool loop...';
|
||||
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--text-dim);padding:8px 0;font-family:var(--font-mono)">${typeMsg}</div></div>`;
|
||||
} else if (job.status === 'failed' && job.error) {
|
||||
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--red);padding:8px 0;font-family:var(--font-mono)">${escHtml(job.error.substring(0,200))}</div></div>`;
|
||||
}
|
||||
|
||||
const queryText = job.created_by ? job.created_by.replace('chat:', '').replace(/session.*/, '') : '';
|
||||
const ts = job.created_at ? new Date(job.created_at).toLocaleTimeString() : '';
|
||||
|
||||
html += `<div class="intel-card${(isOpen && bodyHtml) ? ' open':''}" id="intel-card-${job.id}">
|
||||
<div class="intel-card-head" onclick="toggleIntelCard(${job.id})">
|
||||
<span style="font-size:0.55rem;color:var(--text-dim);font-family:var(--font-mono);flex-shrink:0">${typeLabel}</span>
|
||||
<span class="intel-card-query">#${job.id} ${escHtml((job.created_by||'').replace('chat:','').substring(0,40))}</span>
|
||||
<span style="font-size:0.55rem;color:var(--text-dim);flex-shrink:0;font-family:var(--font-mono)">${ts}</span>
|
||||
<span class="intel-card-status ${statusClass}">${statusLabel}</span>
|
||||
</div>
|
||||
${bodyHtml}
|
||||
</div>`;
|
||||
}
|
||||
el.innerHTML = html;
|
||||
|
||||
} catch(e) {
|
||||
if (el) el.innerHTML = '<div class="intel-empty">INTEL OFFLINE</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleIntelCard(id) {
|
||||
const card = document.getElementById('intel-card-' + id);
|
||||
if (!card) return;
|
||||
if (_intelActiveJobs.has(id)) _intelActiveJobs.delete(id);
|
||||
else _intelActiveJobs.add(id);
|
||||
card.classList.toggle('open');
|
||||
}
|
||||
|
||||
function startIntelPolling() {
|
||||
if (_intelPollTimer) return;
|
||||
_intelPollTimer = setInterval(() => {
|
||||
if (document.getElementById('tab-intel')?.classList.contains('active')) {
|
||||
loadIntel();
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function stopIntelPolling() {
|
||||
if (_intelPollTimer) { clearInterval(_intelPollTimer); _intelPollTimer = null; }
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function intelPrompt() {
|
||||
const input = document.getElementById('textInput');
|
||||
if (input) { input.value = 'research '; input.focus(); }
|
||||
}
|
||||
|
||||
// Called from chat.js when arc_job is returned
|
||||
function onArcJobStarted(jobId, jobType) {
|
||||
_intelActiveJobs.add(jobId);
|
||||
// Auto-switch to INTEL tab
|
||||
const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]');
|
||||
if (intelTab) intelTab.click();
|
||||
startIntelPolling();
|
||||
}
|
||||
|
||||
|
||||
async function loadAgents() {
|
||||
const [listData, metricsData] = await Promise.all([
|
||||
api('agent/list'),
|
||||
|
||||
Reference in New Issue
Block a user