// ============================================ // WeLe Agentic AI — service entrypoint. // // Runs alongside the CRM rather than inside it: the CRM stays a CRM, and the // assistant can be restarted, scaled or given a second channel without // touching it. // ============================================ import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; import config from './config/index.js'; import logger from './utils/logger.js'; import { connectMongo, isMongoConnected } from './data/mongo.js'; import { initRedis, getRedis, redisMode } from './data/redis.js'; import agentRoutes, { artifactRouter } from './gateway/routes.js'; import { ensureDir, sweep } from './output/artifactStore.js'; import crmApi from './tools/http/crmApi.js'; import { describeChains } from './orchestration/llm.js'; import { attachVoice } from './gateway/voice.js'; import { warmup as warmSpeech, speechStatus } from './speech/index.js'; const app = express(); app.use(helmet({ contentSecurityPolicy: false, crossOriginResourcePolicy: false })); app.use(cors({ origin: true, credentials: true })); app.use(express.json({ limit: '2mb' })); app.use((req, _res, next) => { if (req.path !== '/health') logger.debug(`${req.method} ${req.path}`); next(); }); app.get('/health', async (_req, res) => { const crm = await crmApi.health(); const redisOk = await getRedis().ping().then(() => true).catch(() => false); const healthy = isMongoConnected(); res.status(healthy ? 200 : 503).json({ ok: healthy, service: 'wele-agentic-ai', mongo: isMongoConnected() ? 'connected' : 'disconnected', redis: redisOk ? redisMode() : 'unavailable', crm_api: crm.reachable ? 'reachable' : `unreachable (${crm.error || crm.status})`, models: describeChains(), speech: speechStatus(), uptime_s: Math.round(process.uptime()), }); }); app.use('/api/agent', agentRoutes); app.use('/artifacts', artifactRouter); app.use((_req, res) => res.status(404).json({ ok: false, error: 'Not found' })); // eslint-disable-next-line no-unused-vars app.use((err, _req, res, _next) => { logger.error(`unhandled: ${err.stack || err.message}`); res.status(err.status || 500).json({ ok: false, error: err.message || 'Internal error' }); }); async function start() { const configured = Object.entries(config.llm.providers).filter(([, p]) => p.apiKey).map(([n]) => n); if (!configured.length) { logger.error('No provider API key set (GMI_API_KEY / OPENROUTER_API_KEY) — the assistant cannot run. Add one to .env.'); process.exit(1); } logger.info(` providers : ${configured.join(', ')}`); await connectMongo(); initRedis(); await ensureDir(); // Housekeeping for expired generated files. sweep(); setInterval(sweep, 6 * 3600_000).unref(); const server = app.listen(config.port, () => { logger.info(`🤖 WeLe Agentic AI on http://localhost:${config.port}`); const chains = describeChains(); logger.info(` supervisor: ${chains.supervisor.join(' → ')}`); logger.info(` agents: ${chains.agent.join(' → ')}`); logger.info(` CRM API =${config.crmApi.base}`); }); // Voice is a WebSocket upgrade on the same port, so the browser needs no // second origin and the CRM token works unchanged. attachVoice(server); // Speech models load lazily on the first voice turn (~10 s). Set // SPEECH_WARMUP=true to pay that at boot instead — worth it in production, // wasteful in development where most restarts never use voice. if (process.env.SPEECH_WARMUP === 'true') warmSpeech(['ta', 'en']); const shutdown = (sig) => { logger.info(`${sig} — shutting down`); server.close(() => process.exit(0)); setTimeout(() => process.exit(1), 10_000).unref(); }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); } start().catch((err) => { logger.error(`failed to start: ${err.stack || err.message}`); process.exit(1); });