/** * Minimal Express API: /api/agent, /health. * No DB, no auth. CORS allowed for frontend. Production-ready env (PORT, CORS_ORIGIN). */ const express = require('express') const cors = require('cors') const PORT = Number(process.env.PORT) || 8080 const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:3000' const app = express() app.use(cors({ origin: CORS_ORIGIN })) app.use(express.json()) /** Build OpenAI client and full prompt from request body. Returns { openai, modelId, fullPrompt } or { error }. */ function buildAgentRequest(body) { const { prompt, context, contextNodes, connection: conn, reasoning } = body ?? {} const reasoningEnabled = Boolean(reasoning) let baseURL = process.env.AI_BASE_URL?.trim() || null let apiKey = process.env.OPENAI_API_KEY?.trim() || null let modelId = process.env.AI_MODEL?.trim() || (baseURL ? 'local-model' : 'gpt-4o-mini') if (conn && typeof conn === 'object') { const c = conn const provider = c.provider === 'openai' ? 'openai' : 'local' if (provider === 'local') { baseURL = (typeof c.baseURL === 'string' && c.baseURL.trim()) ? c.baseURL.trim() : baseURL apiKey = (typeof c.apiKey === 'string' && c.apiKey.trim()) ? c.apiKey.trim() : (apiKey || 'lm-studio') } else { baseURL = null apiKey = (typeof c.apiKey === 'string' && c.apiKey.trim()) ? c.apiKey.trim() : apiKey } if (typeof c.model === 'string' && c.model.trim()) modelId = c.model.trim() } if (!baseURL && !apiKey) { return { error: 'No AI configured. Set connection in Settings (AI) or env: OPENAI_API_KEY or AI_BASE_URL.' } } const basePrompt = [ typeof prompt === 'string' ? prompt : 'No prompt provided.', context && typeof context === 'string' ? `\n\nAdditional context:\n${context}` : '', Array.isArray(contextNodes) && contextNodes.length > 0 ? `\n\nContext from connected nodes:\n${contextNodes.map((n) => (n.content != null ? n.content : `${n.id}: (no content)`)).join('\n\n')}` : '', ].join('') const fullPrompt = reasoningEnabled ? basePrompt + '\n\nRespond in exactly two markdown sections. First: "## Reasoning" with your step-by-step reasoning. Then: "## Output" with only the final answer. No preamble.' : basePrompt + '\n\nRespond with structured markdown only. No preamble.' const { createOpenAI } = require('@ai-sdk/openai') const openai = createOpenAI({ apiKey: apiKey || 'lm-studio', ...(baseURL && { baseURL, compatibility: 'compatible' }), }) return { openai, modelId, fullPrompt } } /** POST /api/agent — run AI agent; body: { prompt, context?, contextNodes?, connection? }; returns { markdown }. */ app.post('/api/agent', async (req, res) => { try { const built = buildAgentRequest(req.body) if (built.error) return res.status(503).json({ error: built.error }) const { generateText } = await import('ai') const result = await generateText({ model: built.openai(built.modelId), prompt: built.fullPrompt, }) const markdown = result?.text ?? '' res.json({ markdown }) } catch (err) { console.error('Agent error:', err) res.status(500).json({ error: err?.message ?? 'Agent request failed' }) } }) /** POST /api/agent/stream — same as /api/agent but streams plain text (markdown) chunks. */ app.post('/api/agent/stream', async (req, res) => { try { const built = buildAgentRequest(req.body) if (built.error) return res.status(503).json({ error: built.error }) const { streamText } = await import('ai') const result = streamText({ model: built.openai(built.modelId), prompt: built.fullPrompt, }) res.setHeader('Content-Type', 'text/plain; charset=utf-8') res.setHeader('Transfer-Encoding', 'chunked') result.pipeTextStreamToResponse(res) } catch (err) { console.error('Agent stream error:', err) res.status(500).json({ error: err?.message ?? 'Agent request failed' }) } }) /** Health check for Docker / orchestration */ app.get('/health', (req, res) => { res.status(200).json({ ok: true }) }) app.listen(PORT, '0.0.0.0', () => { console.log(`Backend listening on port ${PORT} (CORS: ${CORS_ORIGIN})`) })