// relay v0.3.0 // Core relay server for Lyra project // Handles incoming chat requests and forwards them to Cortex services import express from "express"; import dotenv from "dotenv"; import cors from "cors"; dotenv.config(); const app = express(); app.use(cors()); app.use(express.json()); const PORT = Number(process.env.PORT || 7078); // Cortex endpoints const CORTEX_REASON = process.env.CORTEX_REASON_URL || "http://cortex:7081/reason"; const CORTEX_SIMPLE = process.env.CORTEX_SIMPLE_URL || "http://cortex:7081/simple"; // ----------------------------------------------------- // Helper request wrapper // ----------------------------------------------------- async function postJSON(url, data) { const resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); const raw = await resp.text(); let json; try { json = raw ? JSON.parse(raw) : null; } catch (e) { throw new Error(`Non-JSON from ${url}: ${raw}`); } if (!resp.ok) { throw new Error(json?.detail || json?.error || raw); } return json; } // ----------------------------------------------------- // The unified chat handler // ----------------------------------------------------- async function handleChatRequest(session_id, user_msg, mode = "cortex", backend = null) { let reason; // Determine which endpoint to use based on mode const endpoint = mode === "standard" ? CORTEX_SIMPLE : CORTEX_REASON; const modeName = mode === "standard" ? "simple" : "reason"; console.log(`Relay → routing to Cortex.${modeName} (mode: ${mode}${backend ? `, backend: ${backend}` : ''})`); // Build request payload const payload = { session_id, user_prompt: user_msg }; // Add backend parameter if provided (only for standard mode) if (backend && mode === "standard") { payload.backend = backend; } // Call appropriate Cortex endpoint try { reason = await postJSON(endpoint, payload); } catch (e) { console.error(`Relay → Cortex.${modeName} error:`, e.message); throw new Error(`cortex_${modeName}_failed: ${e.message}`); } // Correct persona field const persona = reason.persona || reason.final_output || "(no persona text)"; // Return final answer return { session_id, reply: persona }; } // ----------------------------------------------------- // HEALTHCHECK // ----------------------------------------------------- app.get("/_health", (_, res) => { res.json({ ok: true }); }); // ----------------------------------------------------- // OPENAI-COMPATIBLE ENDPOINT // ----------------------------------------------------- app.post("/v1/chat/completions", async (req, res) => { try { const session_id = req.body.session_id || req.body.sessionId || req.body.user || "default"; const messages = req.body.messages || []; const lastMessage = messages[messages.length - 1]; const user_msg = lastMessage?.content || ""; const mode = req.body.mode || "cortex"; // Get mode from request, default to cortex const backend = req.body.backend || null; // Get backend preference if (!user_msg) { return res.status(400).json({ error: "No message content provided" }); } console.log(`Relay (v1) → received: "${user_msg}" [mode: ${mode}${backend ? `, backend: ${backend}` : ''}]`); const result = await handleChatRequest(session_id, user_msg, mode, backend); res.json({ id: `chatcmpl-${Date.now()}`, object: "chat.completion", created: Math.floor(Date.now() / 1000), model: "lyra", choices: [{ index: 0, message: { role: "assistant", content: result.reply }, finish_reason: "stop" }], usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } }); } catch (err) { console.error("Relay v1 fatal:", err); res.status(500).json({ error: { message: err.message || String(err), type: "server_error", code: "relay_failed" } }); } }); // ----------------------------------------------------- // MAIN ENDPOINT (Lyra-native UI) // ----------------------------------------------------- app.post("/chat", async (req, res) => { try { const session_id = req.body.session_id || "default"; const user_msg = req.body.message || ""; const mode = req.body.mode || "cortex"; // Get mode from request, default to cortex const backend = req.body.backend || null; // Get backend preference console.log(`Relay → received: "${user_msg}" [mode: ${mode}${backend ? `, backend: ${backend}` : ''}]`); const result = await handleChatRequest(session_id, user_msg, mode, backend); res.json(result); } catch (err) { console.error("Relay fatal:", err); res.status(500).json({ error: "relay_failed", detail: err.message || String(err) }); } }); // ----------------------------------------------------- // SESSION ENDPOINTS (for UI) // ----------------------------------------------------- // In-memory session storage (could be replaced with a database) const sessions = new Map(); app.get("/sessions/:id", (req, res) => { const sessionId = req.params.id; const history = sessions.get(sessionId) || []; res.json(history); }); app.post("/sessions/:id", (req, res) => { const sessionId = req.params.id; const history = req.body; sessions.set(sessionId, history); res.json({ ok: true, saved: history.length }); }); // ----------------------------------------------------- app.listen(PORT, () => { console.log(`Relay is online on port ${PORT}`); });