Files
Agentic-AI/src/server.js
T

106 lines
3.9 KiB
JavaScript

// ============================================
// 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);
});