// utils/chatFreeEndpoint.js import path from "path"; import { fileURLToPath } from "url"; import { vectorSearch } from "./vectorSearch.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootPath = path.resolve(__dirname, "..", ".."); // backend/ const FAQ_PATH = path.join(rootPath, "user_profile.db"); const FAQ_THRESHOLD = 0.80; const HELP_TRIGGERS = ["help","error","bug","issue","login","password","support"]; /* ---------- helpers ---------- */ const classifyIntent = txt => HELP_TRIGGERS.some(k => txt.toLowerCase().includes(k)) ? "support" : "guide"; const buildContext = (user = {}, page = "", intent = "guide") => { const { firstname = "User", career_situation = "planning" } = user; const mode = intent === "guide" ? "Aptiva Guide" : "Aptiva Support"; return { system: `${mode} for page “${page}”. User: ${firstname}. Situation: ${career_situation}.` + (intent === "support" ? " Resolve issues quickly and end with: “Let me know if that fixed it.”" : "") }; }; /* ---------------------------------------------------------------------------- FACTORY: registers POST /api/chat/free on the passed-in Express app ----------------------------------------------------------------------------- */ export default function chatFreeEndpoint( app, { openai, chatLimiter, userProfileDb, authenticateUser = (_req, _res, next) => next() } ) { /* -------- support-intent tool handlers now in scope -------- */ const toolResolvers = { async clearLocalCache() { return { status: "ok" }; }, async openTicket({ summary = "", user = {} }) { try { await userProfileDb.run( `INSERT INTO support_tickets (user_id, summary, created_at) VALUES (?,?,datetime('now'))`, [user.id || 0, summary] ); return { ticket: "created" }; } catch (err) { console.error("[openTicket] DB error:", err); return { ticket: "error" }; } }, async pingStatus() { try { await userProfileDb.get("SELECT 1"); return { status: "healthy" }; } catch { return { status: "db_error" }; } } }; const SUPPORT_TOOLS = [ { type: "function", function: { name: "clearLocalCache", description: "Instruct front-end to purge its cache", parameters: { type: "object", properties: {} } } }, { type: "function", function: { name: "openTicket", description: "Create a support ticket", parameters: { type: "object", properties: { summary: { type: "string" } }, required: ["summary"] } } }, { type: "function", function: { name: "pingStatus", description: "Ping DB/health", parameters: { type: "object", properties: {} } } } ]; /* ----------------------------- ROUTE ----------------------------- */ app.post( "/api/chat/free", chatLimiter, authenticateUser, async (req, res) => { try { const { prompt = "", chatHistory = [], pageContext = "" } = req.body || {}; if (!prompt.trim()) return res.status(400).json({ error: "Empty prompt" }); /* ---------- 0️⃣ FAQ fast-path ---------- */ let faqHit = null; try { const { data } = await openai.embeddings.create({ model: "text-embedding-3-small", input: prompt }); const hits = await vectorSearch(FAQ_PATH, data[0].embedding, 1); if (hits.length && hits[0].score >= FAQ_THRESHOLD) faqHit = hits[0]; } catch { /* silently ignore if table/function missing */ } if (faqHit) return res.json({ answer: faqHit.answer }); /* --------------------------------------- */ const intent = classifyIntent(prompt); const { system } = buildContext(req.user || {}, pageContext, intent); const tools = intent === "support" ? SUPPORT_TOOLS : []; const messages = [ { role:"system", content: system }, ...chatHistory, { role:"user", content: prompt } ]; const chatStream = await openai.chat.completions.create({ model : "gpt-4o-mini", stream : true, messages, tools, tool_choice : tools.length ? "auto" : undefined }); /* ---------- SSE headers ---------- */ res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); for await (const part of chatStream) { const delta = part.choices?.[0]?.delta || {}; const chunk = delta.content || ""; if (chunk) res.write(chunk); // ← plain text, no “data:” prefix } res.end(); /* ---------- tool calls ---------- */ chatStream.on("tool", async call => { const fn = toolResolvers[call.name]; if (!fn) return; try { const args = JSON.parse(call.arguments || "{}"); await fn({ ...args, user: req.user || {} }); } catch (err) { console.error("[tool resolver]", err); } }); } catch (err) { console.error("/api/chat/free error:", err); res.status(500).json({ error: "Internal server error" }); } } ); }