251 lines
7.7 KiB
JavaScript
251 lines
7.7 KiB
JavaScript
require ('dotenv').config();
|
|
const express = require('express');
|
|
const {getEnv, PORTS, EPISODIC} = require('@nexusai/shared');
|
|
const { getDB } = require('./db');
|
|
const { createProject, getProjects, getProject, updateProject, deleteProject } = require('./db/projects');
|
|
|
|
const episodic = require('./episodic');
|
|
const semantic = require('./semantic');
|
|
const entities = require('./entities');
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
const PORT = getEnv('PORT', PORTS.MEMORY);
|
|
|
|
//initialize database on startup
|
|
const db = getDB();
|
|
|
|
semantic.initCollections()
|
|
.then(() => console.log(`QDrant collections ready`))
|
|
.catch(err => console.error(`QDrant initialization error:`, err.message));
|
|
|
|
// Health check endpoint
|
|
app.get('/health', (req, res) => {
|
|
res.json({ service: 'Memory Service', status: 'healthy' });
|
|
});
|
|
|
|
/************************************ */
|
|
/********** Session Routes ********** */
|
|
/************************************ */
|
|
|
|
// Creates a new session with an external ID and optional metadata
|
|
app.get('/sessions', (req, res) => {
|
|
const {
|
|
limit = EPISODIC.DEFAULT_PAGE_SIZE,
|
|
offset = EPISODIC.DEFAULT_OFFSET,
|
|
projectId
|
|
} = req.query;
|
|
|
|
const parsedProjectId = projectId && projectId !== 'null' ? Number(projectId) : null;
|
|
|
|
const sessions = episodic.getSessions(Number(limit), Number(offset), parsedProjectId);
|
|
res.json(sessions);
|
|
});
|
|
|
|
app.post('/sessions', (req, res) => {
|
|
const { externalId, metadata } = req.body;
|
|
if (!externalId) {
|
|
return res.status(400).json({ error: 'externalId is required' });
|
|
}
|
|
try {
|
|
const session = episodic.createSession(externalId, metadata);
|
|
res.status(201).json(session);
|
|
} catch (err) {
|
|
res.status(409).json({ error: 'Session already exists', detail: err.message });
|
|
}
|
|
});
|
|
|
|
// Retrieves a session by its external ID
|
|
app.get('/sessions/by-external/:externalId', (req, res) => {
|
|
const session = episodic.getSessionByExternalId(req.params.externalId);
|
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
res.json(session);
|
|
});
|
|
|
|
|
|
|
|
// Retrieves a session by its internal ID
|
|
app.get('/sessions/:id', (req, res) => {
|
|
const session = episodic.getSession(req.params.id);
|
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
res.json(session);
|
|
});
|
|
|
|
app.patch('/sessions/by-external/:externalId', (req, res) => {
|
|
const { name, projectId } = req.body;
|
|
try {
|
|
const session = episodic.updateSessionByExternalId(req.params.externalId, {name, projectId });
|
|
res.json(session);
|
|
} catch (err) {
|
|
res.status(500).json({error: err.message });
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// Updates the session's updated_at timestamp to now
|
|
app.delete('/sessions/by-external/:externalId', (req, res) => {
|
|
episodic.deleteSessionByExternalId(req.params.externalId);
|
|
res.status(204).send();
|
|
});
|
|
|
|
|
|
/************************************* */
|
|
/********** Episodic Routes ********** */
|
|
/************************************* */
|
|
|
|
app.post('/episodes', async (req, res) => {
|
|
const { sessionId, userMessage, aiResponse, tokenCount, metadata } = req.body;
|
|
if (!sessionId || !userMessage || !aiResponse) {
|
|
return res.status(400).json({ error: 'sessionId, userMessage and aiResponse are required' });
|
|
}
|
|
const episode = await episodic.createEpisode(sessionId, userMessage, aiResponse, tokenCount, metadata);
|
|
|
|
console.log('[memory] create episode body:', {
|
|
sessionId,
|
|
userMessageLength: userMessage?.length,
|
|
aiResponseLength: aiResponse?.length,
|
|
tokenCount
|
|
});
|
|
|
|
res.status(201).json(episode);
|
|
});
|
|
|
|
// Search MUST come before /:id — otherwise 'search' gets captured as an id
|
|
app.get('/episodes/search', (req, res) => {
|
|
const { q, limit = EPISODIC.DEFAULT_PAGE_SIZE } = req.query;
|
|
if (!q) return res.status(400).json({ error: 'q (query) parameter is required' });
|
|
const results = episodic.searchEpisodes(q, Number(limit));
|
|
res.json(results);
|
|
});
|
|
|
|
app.get('/episodes/:id', (req, res) => {
|
|
const episode = episodic.getEpisode(req.params.id);
|
|
if (!episode) return res.status(404).json({ error: 'Episode not found' });
|
|
res.json(episode);
|
|
});
|
|
|
|
// Get paginated episodes for a session
|
|
app.get('/sessions/:id/episodes', (req, res) => {
|
|
const { limit = EPISODIC.DEFAULT_PAGE_SIZE, offset = EPISODIC.DEFAULT_OFFSET } = req.query;
|
|
const episodes = episodic.getEpisodesBySession(
|
|
req.params.id,
|
|
Number(limit),
|
|
Number(offset)
|
|
);
|
|
res.json(episodes);
|
|
});
|
|
|
|
app.delete('/episodes/:id', (req, res) => {
|
|
episodic.deleteEpisode(req.params.id);
|
|
res.status(204).send();
|
|
});
|
|
|
|
/*********************************** */
|
|
/********** Entity Routes ********** */
|
|
/*********************************** */
|
|
//Upsert an entity, creates or updates if already exists
|
|
app.post('/entities', (req, res) => {
|
|
const {name, type, notes, metadata} = req.body;
|
|
if (!name || !type) {
|
|
return res.status(400).json({ error: 'name and type are required' });
|
|
}
|
|
const entity = entities.upsertEntity(name, type, notes, metadata);
|
|
res.status(201).json(entity);
|
|
});
|
|
|
|
// Get all entities of a given type
|
|
app.get('/entities/by-type/:type', (req, res) => {
|
|
res.json(entities.getEntitiesByType(req.params.type));
|
|
});
|
|
|
|
// Get an entity by ID
|
|
app.get('/entities/:id', (req, res) => {
|
|
const entity = entities.getEntity(req.params.id);
|
|
if (!entity) return res.status(404).json({ error: 'Entity not found' });
|
|
res.json(entity);
|
|
});
|
|
|
|
|
|
|
|
// Delete an entity by ID
|
|
app.delete('/entities/:id', (req, res) => {
|
|
entities.deleteEntity(req.params.id);
|
|
res.status(204).send();
|
|
});
|
|
|
|
/***************************************** */
|
|
/********** Relationship Routes ********** */
|
|
/***************************************** */
|
|
|
|
// Upsert a relationship between two entities
|
|
app.post('/relationships', (req, res) => {
|
|
const {fromId, toId, label, metadata } = req.body;
|
|
if (!fromId || !toId || !label) {
|
|
return res.status(400).json({ error: 'fromId, toId and label are required' });
|
|
}
|
|
const relationship = entities.upsertRelationship(fromId, toId, label, metadata);
|
|
res.status(201).json(relationship);
|
|
});
|
|
|
|
// Get all relationships for a given entity ID
|
|
app.get('/entities/:id/relationships', (req, res) => {
|
|
res.json(entities.getRelationshipsByEntity(req.params.id));
|
|
});
|
|
|
|
// Delete a specific relationship
|
|
app.delete('/relationships', (req, res) => {
|
|
const {fromId, toId, label} = req.body;
|
|
if (!fromId || !toId || !label) {
|
|
return res.status(400).json({ error: 'fromId, toId and label are required' });
|
|
}
|
|
entities.deleteRelationship(fromId, toId, label);
|
|
res.status(204).send();
|
|
})
|
|
|
|
/*********************************** */
|
|
/********** Project Routes ********** */
|
|
/*********************************** */
|
|
|
|
app.post('/projects', (req, res) => {
|
|
const { name, description, colour, icon } = req.body;
|
|
if (!name?.trim()) return res.status(400).json({ error: 'name is required' });
|
|
try {
|
|
res.status(201).json(createProject({ name: name.trim(), description, colour, icon }));
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
app.get('/projects', (req, res) => {
|
|
res.json(getProjects());
|
|
});
|
|
|
|
app.get('/projects/:id', (req, res) => {
|
|
const project = getProject(req.params.id);
|
|
if (!project) return res.status(404).json({ error: 'Not found' });
|
|
res.json(project);
|
|
});
|
|
|
|
app.patch('/projects/:id', (req, res) => {
|
|
const project = getProject(req.params.id);
|
|
if (!project) return res.status(404).json({ error: 'Not found' });
|
|
res.json(updateProject(req.params.id, req.body));
|
|
});
|
|
|
|
app.delete('/projects/:id', (req, res) => {
|
|
const project = getProject(req.params.id);
|
|
if (!project) return res.status(404).json({ error: 'Not found' });
|
|
deleteProject(req.params.id);
|
|
res.status(204).send();
|
|
});
|
|
|
|
|
|
|
|
/********************************** */
|
|
/********** Start Server ********** */
|
|
/********************************** */
|
|
app.listen(PORT, () => {
|
|
console.log(`Memory Service is running on port ${PORT}`);
|
|
}); |