[object Object]

← back to AbramsEgo

feat(chat): local-ollama AI chat panel ($0, cap-relieving)

672e1bc35c55aabcc02f64bdeeacef6e21868482 · 2026-07-01 11:20:06 -0700 · Steve

Files touched

Diff

commit 672e1bc35c55aabcc02f64bdeeacef6e21868482
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 1 11:20:06 2026 -0700

    feat(chat): local-ollama AI chat panel ($0, cap-relieving)
---
 public/index.html |  2 +-
 server.js         | 80 ++++++++++++++++++++++++++++++++++++++++++++-----------
 2 files changed, 65 insertions(+), 17 deletions(-)

diff --git a/public/index.html b/public/index.html
index 0540bfb..21c718c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -344,7 +344,7 @@ $('q').addEventListener('keydown',async e=>{
   $('ans').textContent='…thinking';
   try{
     const r=await (await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({q})})).json();
-    $('ans').textContent=(r.answer||r.error||'no answer')+`\n\n— ${r.engine||'?'} · cost ${usd(r.cost_usd||0)}`;
+    $('ans').textContent=(r.answer||r.error||'no answer')+`\n\n— ${r.engine||'?'} · cost ${r.cost||'$0 (local ollama)'}`;
   }catch(e){ $('ans').textContent='chat error'; }
 });
 
diff --git a/server.js b/server.js
index aad33c4..a7c5d42 100644
--- a/server.js
+++ b/server.js
@@ -575,26 +575,74 @@ app.post('/api/revenue/record', async (req, res) => {
   res.json({ ok: true, recorded: row, pnl: SNAP.pnl && SNAP.pnl.today });
 });
 
-// AI chat — grounded in the current snapshot. Uses local Ollama if reachable
-// (unmetered, $0); otherwise returns a deterministic grounded summary. No paid
-// API is called from here without Steve wiring it — cost line always shown.
+// === maxit-09: AI chat over the snapshot, LOCAL OLLAMA ($0, cap-relieving) ===
+// Natural-language Q&A grounded in the current snapshot. Uses LOCAL Ollama so it
+// is $0/unmetered and does NOT consume the Anthropic Max session cap. Endpoints
+// are tried in priority order — Mac1 (192.168.1.133) first, then Mac2 localhost.
+// If neither is reachable, a deterministic grounded summary is returned; no paid
+// API is ever called. The cost line "$0 (local ollama)" is ALWAYS in the payload.
+const OLLAMA_ENDPOINTS = (process.env.OLLAMA_URLS
+  ? process.env.OLLAMA_URLS.split(',').map((s) => s.trim()).filter(Boolean)
+  : ['http://192.168.1.133:11434', 'http://localhost:11434']);
+const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'qwen3:14b';
+const OLLAMA_TIMEOUT_MS = parseInt(process.env.OLLAMA_TIMEOUT_MS, 10) || 8000;
+const CHAT_COST_LINE = '$0 (local ollama)';
+
+// Build a compact, human-readable context string from the current snapshot
+// (costs, fleet, crons, sessions) so the local model has grounding without the
+// full JSON blowing the prompt budget.
+function chatContext() {
+  const f = SNAP || {};
+  const c = f.cost && f.cost.today;
+  const p = f.pnl && f.pnl.today;
+  const cr = f.crons;
+  const u = f.usageSummary || {};
+  const canDown = (f.canaries && f.canaries.canaries || []).filter((x) => x.verdict && /DOWN|FAIL|DEGRADED/i.test(x.verdict));
+  return [
+    `Snapshot built: ${f.builtAt || 'n/a'} (AbramsEgo v${f.version || '?'}).`,
+    `System: load ${f.system && f.system.loadPct}% of ${f.system && f.system.cpus} cores, RAM ${f.system && f.system.memUsedPct}% used, disk ${f.system && f.system.diskUsedPct}%.`,
+    `Local fleet (Mac2 pm2): ${f.localFleet && f.localFleet.up}/${f.localFleet && f.localFleet.total} online.`,
+    `Kamatera fleet: ${f.kamatera && f.kamatera.up}/${f.kamatera && f.kamatera.total} online.`,
+    `Crons/launchd: ${cr && cr.loadedCount}/${cr && cr.count} loaded.`,
+    `Cost today: $${c && c.total} total (tokens $${c && c.tokens} + energy $${c && c.energy} est). Self-funding today: ${p && p.selfFundingPct}%.`,
+    `Sessions: ${u.activeSessions} active, ${u.recentSessions} recent(24h); sub-agent runs ${u.subagentRuns}; top model ${u.topModel}.`,
+    `Wins today: ${f.wins && f.wins.today}.`,
+    canDown.length ? `Canaries needing attention: ${canDown.map((x) => `${x.name}=${x.verdict}`).join(', ')}.` : 'All reporting canaries healthy.',
+  ].join('\n');
+}
+
+// POST one prompt to a single Ollama endpoint (non-streaming) with a hard timeout.
+async function askOllama(endpoint, prompt) {
+  const ctrl = new AbortController();
+  const t = setTimeout(() => ctrl.abort(), OLLAMA_TIMEOUT_MS);
+  try {
+    const r = await fetch(`${endpoint}/api/generate`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ model: OLLAMA_MODEL, stream: false, prompt }),
+      signal: ctrl.signal,
+    });
+    if (!r.ok) return null;
+    const j = await r.json();
+    const answer = (j.response || '').trim();
+    return answer || null;
+  } catch (e) { return null; } finally { clearTimeout(t); }
+}
+
 app.post('/api/chat', async (req, res) => {
   const q = (req.body && req.body.q || '').toString().slice(0, 500);
   if (!q) return res.status(400).json({ error: 'empty question' });
-  const ctx = JSON.stringify({ system: SNAP.system, fleet: { local: SNAP.localFleet, kamatera: SNAP.kamatera }, canaries: SNAP.canaries, cost: SNAP.cost, pnl: SNAP.pnl, wins: SNAP.wins }).slice(0, 6000);
-  const OLLAMA = process.env.OLLAMA_URL || 'http://127.0.0.1:11434';
-  const body = { model: process.env.OLLAMA_MODEL || 'qwen2.5:7b', stream: false, prompt: `You are AbramsEgo, Steve's fleet command center. Answer concisely using ONLY this live snapshot JSON:\n${ctx}\n\nQuestion: ${q}\nAnswer:` };
-  try {
-    const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 20000);
-    const r = await fetch(`${OLLAMA}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: ctrl.signal });
-    clearTimeout(t);
-    if (r.ok) { const j = await r.json(); return res.json({ answer: (j.response || '').trim(), engine: 'ollama(local)', cost_usd: 0 }); }
-  } catch (e) {}
-  // deterministic fallback
-  const f = SNAP;
-  const answer = `Snapshot ${f.builtAt}. Local fleet ${f.localFleet && f.localFleet.up}/${f.localFleet && f.localFleet.total} up; Kamatera ${f.kamatera && f.kamatera.up}/${f.kamatera && f.kamatera.total} up. Cost today $${f.cost && f.cost.today && f.cost.today.total} (tokens $${f.cost && f.cost.today && f.cost.today.tokens} + energy $${f.cost && f.cost.today && f.cost.today.energy} est). Self-funding today: ${f.pnl && f.pnl.today && f.pnl.today.selfFundingPct}%. Wins today: ${f.wins && f.wins.today}.`;
-  res.json({ answer, engine: 'local-template', cost_usd: 0 });
+  const ctx = chatContext();
+  const prompt = `You are AbramsEgo, Steve's local AI-agent command center. Answer the question concisely and factually using ONLY this live fleet snapshot:\n\n${ctx}\n\nQuestion: ${q}\nAnswer:`;
+  // Try each Ollama endpoint in priority order (Mac1 → Mac2 fallback).
+  for (const endpoint of OLLAMA_ENDPOINTS) {
+    const answer = await askOllama(endpoint, prompt);
+    if (answer) return res.json({ answer, engine: `ollama:${OLLAMA_MODEL}@${endpoint.replace(/^https?:\/\//, '')}`, cost: CHAT_COST_LINE, cost_usd: 0 });
+  }
+  // Neither Ollama reachable → deterministic grounded summary (never a paid API).
+  res.json({ answer: `No local Ollama reachable — grounded summary instead:\n${ctx}`, engine: 'local-template', cost: CHAT_COST_LINE, cost_usd: 0 });
 });
+// === end maxit-09 chat ======================================================
 
 // --- SELL-IT landing (local, publish-gated) --------------------------------
 // Explicit route so /landing serves the marketing page even though it's still

← 0e2c42b feat: local sell-it landing + pricing + waitlist (publish ga  ·  back to AbramsEgo  ·  feat: cross-repo git activity feed d24ccf1 →