← back to AbramsEgo
feat(chat): local-ollama AI chat panel ($0, cap-relieving)
672e1bc35c55aabcc02f64bdeeacef6e21868482 · 2026-07-01 11:20:06 -0700 · Steve
Files touched
M public/index.htmlM server.js
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 →