import express from "express"; import dotenv from "dotenv"; import cors from "cors"; import fs from "fs"; import path from "path"; import { reflectWithCortex, ingestToCortex } from "./lib/cortex.js"; dotenv.config(); const sessionsDir = path.join(process.cwd(), "sessions"); if (!fs.existsSync(sessionsDir)) fs.mkdirSync(sessionsDir); const app = express(); app.use(cors()); app.use(express.json()); // Cache and normalize env flags/values once const { NEOMEM_API, MEM0_API_KEY, OPENAI_API_KEY, OLLAMA_URL, PERSONA_URL, CORTEX_ENABLED, PORT: PORT_ENV, DEBUG_PROMPT, } = process.env; const PORT = Number(PORT_ENV) || 7078; const cortexEnabled = String(CORTEX_ENABLED).toLowerCase() === "true"; const debugPrompt = String(DEBUG_PROMPT).toLowerCase() === "true"; // Basic env validation warnings (non-fatal) if (!NEOMEM_API || !MEM0_API_KEY) { console.warn("⚠️ NeoMem configuration missing: NEOMEM_API or MEM0_API_KEY not set."); } /* ------------------------------ Helpers for NeoMem REST API --------------------------------*/ // Small helper for fetch with timeout + JSON + error detail async function fetchJSON(url, options = {}, timeoutMs = 30000) { const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs); try { const resp = await fetch(url, { ...options, signal: controller.signal }); const text = await resp.text(); const parsed = text ? JSON.parse(text) : null; if (!resp.ok) { const msg = parsed?.error || parsed?.message || text || resp.statusText; throw new Error(`${resp.status} ${msg}`); } return parsed; } finally { clearTimeout(t); } } async function memAdd(content, userId, sessionId, cortexData) { const url = `${NEOMEM_API}/memories`; const payload = { messages: [{ role: "user", content }], user_id: userId, // run_id: sessionId, metadata: { source: "relay", cortex: cortexData }, }; return fetchJSON(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${MEM0_API_KEY}`, }, body: JSON.stringify(payload), }); } async function memSearch(query, userId, sessionId) { const url = `${NEOMEM_API}/search`; const payload = { query, user_id: userId }; return fetchJSON(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${MEM0_API_KEY}`, }, body: JSON.stringify(payload), }); } /* ------------------------------ Utility to time spans --------------------------------*/ async function span(name, fn) { const start = Date.now(); try { return await fn(); } finally { console.log(`${name} took ${Date.now() - start}ms`); } } /* ------------------------------ Healthcheck --------------------------------*/ app.get("/_health", (req, res) => { res.json({ ok: true, time: new Date().toISOString() }); }); /* ------------------------------ Sessions --------------------------------*/ // List all saved sessions app.get("/sessions", (_, res) => { const list = fs.readdirSync(sessionsDir) .filter(f => f.endsWith(".json")) .map(f => f.replace(".json", "")); res.json(list); }); // Load a single session app.get("/sessions/:id", (req, res) => { const file = path.join(sessionsDir, `${req.params.id}.json`); if (!fs.existsSync(file)) return res.json([]); res.json(JSON.parse(fs.readFileSync(file, "utf8"))); }); // Save or update a session app.post("/sessions/:id", (req, res) => { const file = path.join(sessionsDir, `${req.params.id}.json`); fs.writeFileSync(file, JSON.stringify(req.body, null, 2)); res.json({ ok: true }); }); /* ------------------------------ Chat completion endpoint --------------------------------*/ app.post("/v1/chat/completions", async (req, res) => { try { const { model, messages, sessionId: clientSessionId } = req.body || {}; if (!Array.isArray(messages) || !messages.length) { return res.status(400).json({ error: "invalid_messages" }); } if (!model || typeof model !== "string") { return res.status(400).json({ error: "invalid_model" }); } const sessionId = clientSessionId || "default"; const userId = "brian"; // fixed for now console.log(`🛰️ Incoming request. Session: ${sessionId}`); // Find last user message efficiently const lastUserMsg = [...messages].reverse().find(m => m.role === "user")?.content; if (!lastUserMsg) { return res.status(400).json({ error: "no_user_message" }); } // 1. Cortex Reflection (new pipeline) /*let reflection = {}; try { console.log("🧠 Reflecting with Cortex..."); const memoriesPreview = []; // we'll fill this in later with memSearch reflection = await reflectWithCortex(lastUserMsg, memoriesPreview); console.log("🔍 Reflection:", reflection); } catch (err) { console.warn("⚠️ Cortex reflect failed:", err.message); reflection = { error: err.message }; }*/ // 2. Search memories /* let memorySnippets = []; await span("mem.search", async () => { if (NEOMEM_API && MEM0_API_KEY) { try { const { results } = await memSearch(lastUserMsg, userId, sessionId); if (results?.length) { console.log(`📚 Mem0 hits: ${results.length}`); results.forEach((r, i) => console.log(` ${i + 1}) ${r.memory} (score ${Number(r.score).toFixed(3)})`) ); memorySnippets = results.map((r, i) => `${i + 1}) ${r.memory}`); } else { console.log("😴 No memories found"); } } catch (e) { console.warn("⚠️ mem.search failed:", e.message); } } });*/ // 3. Fetch persona /* let personaText = "Persona: Lyra 🤖 friendly, concise, poker-savvy."; await span("persona.fetch", async () => { try { if (PERSONA_URL) { const data = await fetchJSON(PERSONA_URL); if (data?.persona) { const name = data.persona.name ?? "Lyra"; const style = data.persona.style ?? "friendly, concise"; const protocols = Array.isArray(data.persona.protocols) ? data.persona.protocols.join(", ") : ""; personaText = `Persona: ${name} 🤖 ${style}. Protocols: ${protocols}`.trim(); } } } catch (err) { console.error("💥 persona.fetch failed", err); } }); */ // 1. Ask Cortex to build the final prompt let cortexPrompt = ""; try { console.log("🧠 Requesting prompt from Cortex..."); const response = await fetch(`${process.env.CORTEX_API_URL || "http://10.0.0.41:7081"}/reason`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: lastUserMsg, session_id: sessionId, user_id: userId }) }); const data = await response.json(); cortexPrompt = data.full_prompt || data.prompt || ""; console.log("🧩 Cortex returned prompt"); } catch (err) { console.warn("⚠️ Cortex prompt build failed:", err.message); } // 4. Build final messages const injectedMessages = [ { role: "system", content: cortexPrompt || "You are Lyra." }, ...messages, ]; if (debugPrompt) { console.log("\n==== Injected Prompt ===="); console.log(JSON.stringify(injectedMessages, null, 2)); console.log("=========================\n"); } // 5. Call LLM (OpenAI or Ollama) const isOllama = model.startsWith("ollama:"); const llmUrl = isOllama ? `${OLLAMA_URL}/api/chat` : "https://api.openai.com/v1/chat/completions"; const llmHeaders = isOllama ? { "Content-Type": "application/json" } : { "Content-Type": "application/json", Authorization: `Bearer ${OPENAI_API_KEY}`, }; const llmBody = { model: isOllama ? model.replace("ollama:", "") : model, messages: injectedMessages, // <-- make sure injectedMessages is defined above this section stream: false, }; const data = await fetchJSON(llmUrl, { method: "POST", headers: llmHeaders, body: JSON.stringify(llmBody), }); // define once for everything below const assistantReply = isOllama ? data?.message?.content : data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || ""; // 🧠 Send exchange back to Cortex for ingest try { await ingestToCortex(lastUserMsg, assistantReply || "", {}, sessionId); console.log("📤 Sent exchange back to Cortex ingest"); } catch (err) { console.warn("⚠️ Cortex ingest failed:", err.message); } // 💾 Save exchange to session log try { const logFile = path.join(sessionsDir, `${sessionId}.jsonl`); const entry = JSON.stringify({ ts: new Date().toISOString(), turn: [ { role: "user", content: lastUserMsg }, { role: "assistant", content: assistantReply || "" } ] }) + "\n"; fs.appendFileSync(logFile, entry, "utf8"); console.log(`🧠 Logged session exchange → ${logFile}`); } catch (e) { console.warn("⚠️ Session log write failed:", e.message); } // 🔄 Forward user↔assistant exchange to Intake summarizer if (process.env.INTAKE_API_URL) { try { const intakePayload = { session_id: sessionId, turns: [ { role: "user", content: lastUserMsg }, { role: "assistant", content: assistantReply || "" } ] }; await fetch(process.env.INTAKE_API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(intakePayload), }); console.log("📨 Sent exchange to Intake summarizer"); } catch (err) { console.warn("⚠️ Intake post failed:", err.message); } } if (isOllama) { res.json({ id: "ollama-" + Date.now(), object: "chat.completion", created: Math.floor(Date.now() / 1000), model, choices: [ { index: 0, message: data?.message || { role: "assistant", content: "" }, finish_reason: "stop", }, ], }); } else { res.json(data); } } catch (err) { console.error("💥 relay error", err); res.status(500).json({ error: "relay_failed", detail: err.message }); } }); /* ------------------------------ Start server --------------------------------*/ app.listen(PORT, () => { console.log(`Relay listening on port ${PORT}`); });