102 lines
4.1 KiB
JavaScript
102 lines
4.1 KiB
JavaScript
/**
|
|
* 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})`)
|
|
})
|