refactoring and clean up
This commit is contained in:
@@ -1,16 +1,13 @@
|
|||||||
require ('dotenv').config();
|
require ('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const {getEnv} = require('@nexusai/shared');
|
const {getEnv, OLLAMA, PORTS} = require('@nexusai/shared');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
const PORT = getEnv('PORT', '3003'); // Default to 3003 if PORT is not set
|
const PORT = getEnv('PORT', PORTS.EMBEDDING); // Default to 3003 if PORT is not set
|
||||||
const OLLAMA_URL = getEnv('OLLAMA_URL', 'http://localhost:11434'); // URL for Ollama API
|
const OLLAMA_URL = getEnv('OLLAMA_URL', OLLAMA.DEFAULT_URL); // URL for Ollama API
|
||||||
const EMBED_MODEL = getEnv('EMBEDDING_MODEL', 'nomic-embed-text'); // Ollama model for embeddings
|
const EMBED_MODEL = getEnv('EMBEDDING_MODEL', OLLAMA.EMBED_MODEL); // Ollama model for embeddings
|
||||||
|
|
||||||
console.log('OLLAMA_URL:', OLLAMA_URL);
|
|
||||||
console.log('EMBED_MODEL:', EMBED_MODEL);
|
|
||||||
|
|
||||||
//OLLAMA embedding helper function
|
//OLLAMA embedding helper function
|
||||||
async function embedText(text) {
|
async function embedText(text) {
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
require ('dotenv').config();
|
require ('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const {getEnv} = require('@nexusai/shared');
|
const {getEnv, PORTS, OLLAMA} = require('@nexusai/shared');
|
||||||
const inferenceRouter = require('./routes/inference');
|
const inferenceRouter = require('./routes/inference');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
const PORT = getEnv('PORT', '3001'); // Default to 3001 if PORT is not set
|
const PORT = getEnv('PORT', PORTS.INFERENCE);
|
||||||
|
const PROVIDER = getEnv('INFERENCE_PROVIDER', 'ollama');
|
||||||
|
const MODEL = getEnv('DEFAULT_MODEL', OLLAMA.OLLAMA_MODEL)
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
service: 'Inference Service',
|
service: 'Inference Service',
|
||||||
status: 'healthy',
|
status: 'healthy',
|
||||||
provider: getEnv('INFERENCE_PROVIDER', 'ollama'),
|
provider: PROVIDER,
|
||||||
model: getEnv('DEFAULT_MODEL', 'llama3.2')
|
model: MODEL
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,32 @@
|
|||||||
const { getEnv } = require('@nexusai/shared');
|
const { getEnv, LLAMACPP, INFERENCE_DEFAULTS } = require('@nexusai/shared');
|
||||||
|
|
||||||
const BASE_URL = getEnv('INFERENCE_URL', 'http://localhost:8080');
|
const BASE_URL = getEnv('INFERENCE_URL', LLAMACPP.DEFAULT_URL);
|
||||||
const DEFAULT_MODEL = getEnv('DEFAULT_MODEL', 'local-model');
|
const DEFAULT_MODEL = getEnv('DEFAULT_MODEL', LLAMACPP.DEFAULT_MODEL);
|
||||||
|
|
||||||
|
function resolveOptions(options) {
|
||||||
|
return {
|
||||||
|
temperature: options.temperature ?? INFERENCE_DEFAULTS.TEMPERATURE,
|
||||||
|
maxTokens: options.maxTokens ?? INFERENCE_DEFAULTS.MAX_TOKENS,
|
||||||
|
topP: options.topP ?? INFERENCE_DEFAULTS.TOP_P,
|
||||||
|
topK: options.topK ?? INFERENCE_DEFAULTS.TOP_K,
|
||||||
|
repeatPenalty: options.repeatPenalty ?? INFERENCE_DEFAULTS.REPEAT_PENALTY,
|
||||||
|
seed: options.seed ?? INFERENCE_DEFAULTS.SEED,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildPayload(prompt, options, stream = false){
|
function buildPayload(prompt, options, stream = false){
|
||||||
|
const opts = resolveOptions(options);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: options.model || DEFAULT_MODEL,
|
model: options.model || DEFAULT_MODEL,
|
||||||
messages: [{ role: 'user', content: prompt }],
|
messages: [{ role: 'user', content: prompt }],
|
||||||
temperature: options.temperature ?? 0.7,
|
temperature: opts.temperature,
|
||||||
max_tokens: options.num_predict ?? 1024,
|
max_tokens: opts.maxTokens,
|
||||||
|
top_p: opts.topP,
|
||||||
|
top_k: opts.topK,
|
||||||
|
repeat_penalty: opts.repeatPenalty,
|
||||||
stream,
|
stream,
|
||||||
|
...(opts.seed !== null && { seed: opts.seed }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
const { Ollama } = require('ollama');
|
const { Ollama } = require('ollama');
|
||||||
const { getEnv } = require('@nexusai/shared');
|
const { getEnv, OLLAMA, INFERENCE_DEFAULTS } = require('@nexusai/shared');
|
||||||
|
|
||||||
const client = new Ollama({ host: getEnv('INFERENCE_URL', 'http://localhost:11434') });
|
const client = new Ollama({ host: getEnv('INFERENCE_URL', OLLAMA.DEFAULT_URL) });
|
||||||
const DEFAULT_MODEL = getEnv('DEFAULT_MODEL', 'companion:latest');
|
const DEFAULT_MODEL = getEnv('DEFAULT_MODEL', OLLAMA.OLLAMA_MODEL);
|
||||||
|
|
||||||
|
function resolveOptions(options){
|
||||||
|
return {
|
||||||
|
temperature: options.temperature ?? INFERENCE_DEFAULTS.TEMPERATURE,
|
||||||
|
maxTokens: options.maxTokens ?? INFERENCE_DEFAULTS.MAX_TOKENS,
|
||||||
|
topP: options.topP ?? INFERENCE_DEFAULTS.TOP_P,
|
||||||
|
topK: options.topK ?? INFERENCE_DEFAULTS.TOP_K,
|
||||||
|
repeatPenalty: options.repeatPenalty ?? INFERENCE_DEFAULTS.REPEAT_PENALTY,
|
||||||
|
seed: options.seed ?? INFERENCE_DEFAULTS.SEED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function complete(prompt, options = {} ) {
|
async function complete(prompt, options = {} ) {
|
||||||
|
const opts = resolveOptions(options);
|
||||||
const response = await client.generate({
|
const response = await client.generate({
|
||||||
model: options.model || DEFAULT_MODEL,
|
model: options.model || DEFAULT_MODEL,
|
||||||
prompt,
|
prompt,
|
||||||
stream: false,
|
stream: false,
|
||||||
options: {
|
options: {
|
||||||
temperature: options.temperature ?? 0.7,
|
temperature: opts.temperature,
|
||||||
num_predict: options.maxTokens ?? 1024,
|
num_predict: opts.maxTokens,
|
||||||
|
top_p: opts.topP,
|
||||||
|
top_k: opts.topK,
|
||||||
|
repeat_penalty: opts.repeatPenalty,
|
||||||
|
...(opts.seed !== null && { seed: opts.seed }),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,12 +41,18 @@ async function complete(prompt, options = {} ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function* completeStream(prompt, options = {} ) {
|
async function* completeStream(prompt, options = {} ) {
|
||||||
|
const opts = resolveOptions(options);
|
||||||
const stream = await client.generate({
|
const stream = await client.generate({
|
||||||
model: options.model || DEFAULT_MODEL,
|
model: options.model || DEFAULT_MODEL,
|
||||||
prompt,
|
prompt,
|
||||||
stream: true,
|
stream: true,
|
||||||
options:{
|
options:{
|
||||||
temperature: options.temperature ?? 0.7,
|
temperature: opts.temperature,
|
||||||
|
num_predict: opts.maxTokens,
|
||||||
|
top_p: opts.topP,
|
||||||
|
top_k: opts.topK,
|
||||||
|
repeat_penalty: opts.repeatPenalty,
|
||||||
|
...(opts.seed !== null && { seed: opts.seed }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
const Database = require('better-sqlite3');
|
const Database = require('better-sqlite3');
|
||||||
const schema = require('./schema');
|
const schema = require('./schema');
|
||||||
const {getEnv } = require('@nexusai/shared');
|
const {getEnv, SQLITE } = require('@nexusai/shared');
|
||||||
|
|
||||||
let db; // Declare db variable in a scope accessible to all functions
|
let db; // Declare db variable in a scope accessible to all functions
|
||||||
|
|
||||||
function getDB() {
|
function getDB() {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
const path = getEnv('SQLITE_PATH', './data/nexusai.db');
|
const path = getEnv('SQLITE_PATH', SQLITE.DEFAULT_PATH);
|
||||||
db = new Database(path);
|
db = new Database(path);
|
||||||
|
|
||||||
db.pragma('journal_mode = WAL');
|
db.pragma('journal_mode = WAL');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const {getDB} = require('../db');
|
const {getDB} = require('../db');
|
||||||
|
const { parseRow } = require ('@nexusai/shared')
|
||||||
|
|
||||||
/******* Entities ********/
|
/******* Entities ********/
|
||||||
|
|
||||||
@@ -21,13 +22,13 @@ function upsertEntity(name, type, notes = null, metadata = null) {
|
|||||||
// Get an entity by its ID
|
// Get an entity by its ID
|
||||||
function getEntity(id) {
|
function getEntity(id) {
|
||||||
const db = getDB();
|
const db = getDB();
|
||||||
return parseEntity(db.prepare(`SELECT * FROM entities WHERE id = ?`).get(id));
|
return parseRow(db.prepare(`SELECT * FROM entities WHERE id = ?`).get(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all entities of a given type
|
// Get all entities of a given type
|
||||||
function getEntitiesByType(type) {
|
function getEntitiesByType(type) {
|
||||||
const db = getDB();
|
const db = getDB();
|
||||||
return db.prepare(`SELECT * FROM entities WHERE type = ? ORDER BY name`).all(type).map(parseEntity);
|
return db.prepare(`SELECT * FROM entities WHERE type = ? ORDER BY name`).all(type).map(parseRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete an entity by ID, cascades to delete relationships involving this entity
|
// Delete an entity by ID, cascades to delete relationships involving this entity
|
||||||
@@ -55,7 +56,7 @@ function upsertRelationship(fromId, toId, label, metadata = null){
|
|||||||
function getRelationship(fromId, toId, label) {
|
function getRelationship(fromId, toId, label) {
|
||||||
const db = getDB();
|
const db = getDB();
|
||||||
|
|
||||||
return parseRelationship(
|
return parseRow(
|
||||||
db.prepare(`SELECT * FROM relationships WHERE from_id = ? AND to_id = ? AND label = ?`)
|
db.prepare(`SELECT * FROM relationships WHERE from_id = ? AND to_id = ? AND label = ?`)
|
||||||
.get(fromId, toId, label)
|
.get(fromId, toId, label)
|
||||||
);
|
);
|
||||||
@@ -64,13 +65,13 @@ function getRelationship(fromId, toId, label) {
|
|||||||
// Retrieves an entity by its unique (name, type) combination
|
// Retrieves an entity by its unique (name, type) combination
|
||||||
function getEntityByNameType(name, type) {
|
function getEntityByNameType(name, type) {
|
||||||
const db = getDB();
|
const db = getDB();
|
||||||
return parseEntity(db.prepare(`SELECT * FROM entities WHERE name = ? AND type = ?`).get(name, type));
|
return parseRow(db.prepare(`SELECT * FROM entities WHERE name = ? AND type = ?`).get(name, type));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrive all relationships originating from a given entity
|
// Retrive all relationships originating from a given entity
|
||||||
function getRelationshipsByEntity(entityId) {
|
function getRelationshipsByEntity(entityId) {
|
||||||
const db = getDB();
|
const db = getDB();
|
||||||
return db.prepare(`SELECT * FROM relationships WHERE from_id = ?`).all(entityId).map(parseRelationship);
|
return db.prepare(`SELECT * FROM relationships WHERE from_id = ?`).all(entityId).map(parseRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a specific relationship by (from_id, to_id, label)
|
// Delete a specific relationship by (from_id, to_id, label)
|
||||||
@@ -80,24 +81,6 @@ function deleteRelationship(fromId, toId, label) {
|
|||||||
db.prepare(`DELETE FROM relationships WHERE from_id = ? AND to_id = ? AND label = ?`).run(fromId, toId, label);
|
db.prepare(`DELETE FROM relationships WHERE from_id = ? AND to_id = ? AND label = ?`).run(fromId, toId, label);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*********** Parse Functions ***********/
|
|
||||||
|
|
||||||
function parseEntity(row) {
|
|
||||||
if (!row) return null;
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseRelationship(row) {
|
|
||||||
if (!row) return null;
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
upsertEntity,
|
upsertEntity,
|
||||||
getEntity,
|
getEntity,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const {getDB} = require('../db');
|
const {getDB} = require('../db');
|
||||||
const { EPISODIC, getEnv, SERVICES } = require('@nexusai/shared');
|
const { EPISODIC, getEnv, SERVICES, parseRow, formatEpisodeText } = require('@nexusai/shared');
|
||||||
const semantic = require('../semantic');
|
const semantic = require('../semantic');
|
||||||
|
|
||||||
// --Sessions --------------------------------------------------
|
// --Sessions --------------------------------------------------
|
||||||
@@ -20,7 +20,7 @@ function createSession(externalId, metadata = null) {
|
|||||||
function getSession(id) {
|
function getSession(id) {
|
||||||
const db = getDB();
|
const db = getDB();
|
||||||
const stmt = db.prepare(`SELECT * FROM sessions WHERE id = ?`);
|
const stmt = db.prepare(`SELECT * FROM sessions WHERE id = ?`);
|
||||||
return parseSession(stmt.get(id));
|
return parseRow(stmt.get(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSessions(limit = EPISODIC.DEFAULT_PAGE_SIZE, offset = 0) {
|
function getSessions(limit = EPISODIC.DEFAULT_PAGE_SIZE, offset = 0) {
|
||||||
@@ -30,14 +30,14 @@ function getSessions(limit = EPISODIC.DEFAULT_PAGE_SIZE, offset = 0) {
|
|||||||
ORDER BY updated_at DESC
|
ORDER BY updated_at DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`);
|
`);
|
||||||
return stmt.all(limit, offset).map(parseSession);
|
return stmt.all(limit, offset).map(parseRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieves a session by its external ID
|
// Retrieves a session by its external ID
|
||||||
function getSessionByExternalId(externalId) {
|
function getSessionByExternalId(externalId) {
|
||||||
const db = getDB();
|
const db = getDB();
|
||||||
const stmt = db.prepare(`SELECT * FROM sessions WHERE external_id = ?`);
|
const stmt = db.prepare(`SELECT * FROM sessions WHERE external_id = ?`);
|
||||||
return parseSession(stmt.get(externalId));
|
return parseRow(stmt.get(externalId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates the updated_at timestamp of a session to the current time
|
// Updates the updated_at timestamp of a session to the current time
|
||||||
@@ -103,7 +103,7 @@ function getEpisodesBySession(sessionId, limit = EPISODIC.DEFAULT_PAGE_SIZE, off
|
|||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`);
|
`);
|
||||||
return stmt.all(sessionId, limit, offset).map(parseEpisode);
|
return stmt.all(sessionId, limit, offset).map(parseRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieves recent episodes across all sessions, ordered by creation time descending, with a limit
|
// Retrieves recent episodes across all sessions, ordered by creation time descending, with a limit
|
||||||
@@ -115,7 +115,7 @@ function getRecentEpisodes(limit = EPISODIC.DEFAULT_RECENT_LIMIT) {
|
|||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`);
|
`);
|
||||||
return stmt.all(limit).map(parseEpisode);
|
return stmt.all(limit).map(parseRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ function searchEpisodes(query, limit = EPISODIC.DEFAULT_SEARCH_LIMIT) {
|
|||||||
ORDER BY rank
|
ORDER BY rank
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`);
|
`);
|
||||||
return stmt.all(query, limit).map(parseEpisode);
|
return stmt.all(query, limit).map(parseRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes an episode by its ID
|
// Deletes an episode by its ID
|
||||||
@@ -139,32 +139,12 @@ function deleteEpisode(id) {
|
|||||||
db.prepare(`DELETE FROM episodes WHERE id = ?`).run(id);
|
db.prepare(`DELETE FROM episodes WHERE id = ?`).run(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Parsers ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Parse JSON metadata back out on the way up — stored as string, returned as object
|
|
||||||
function parseSession(row) {
|
|
||||||
if (!row) return null;
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JSON metadata back out on the way up — stored as string, returned as object
|
|
||||||
function parseEpisode(row) {
|
|
||||||
if (!row) return null;
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/******** Embedding Helper ********/
|
/******** Embedding Helper ********/
|
||||||
async function getEpisodeEmbedding(userMessage, aiResponse){
|
async function getEpisodeEmbedding(userMessage, aiResponse){
|
||||||
const url = getEnv('EMBEDDING_SERVICE_URL', SERVICES.EMBEDDING_URL);
|
const url = getEnv('EMBEDDING_SERVICE_URL', SERVICES.EMBEDDING_URL);
|
||||||
|
|
||||||
//Combine user message and AI response for embedding
|
//Combine user message and AI response for embedding
|
||||||
const text = `User: ${userMessage}\nAssistant: ${aiResponse}`;
|
const text = formatEpisodeText(userMessage, aiResponse);
|
||||||
|
|
||||||
const res = await fetch(`${url}/embed`, {
|
const res = await fetch(`${url}/embed`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
require ('dotenv').config();
|
require ('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const {getEnv} = require('@nexusai/shared');
|
const {getEnv, PORTS, EPISODIC} = require('@nexusai/shared');
|
||||||
const { getDB } = require('./db');
|
const { getDB } = require('./db');
|
||||||
|
|
||||||
const episodic = require('./episodic');
|
const episodic = require('./episodic');
|
||||||
const semantic = require('./semantic');
|
const semantic = require('./semantic');
|
||||||
const entities = require('./entities');
|
const entities = require('./entities');
|
||||||
@@ -9,7 +10,7 @@ const entities = require('./entities');
|
|||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
const PORT = getEnv('PORT', '3002'); // Default to 3002 if PORT is not set
|
const PORT = getEnv('PORT', PORTS.MEMORY);
|
||||||
|
|
||||||
//initialize database on startup
|
//initialize database on startup
|
||||||
const db = getDB();
|
const db = getDB();
|
||||||
@@ -42,7 +43,7 @@ app.post('/sessions', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/sessions', (req, res) => {
|
app.get('/sessions', (req, res) => {
|
||||||
const {limit = 20, offset = 0 } = req.query;
|
const {limit = EPISODIC.DEFAULT_PAGE_SIZE, offset = EPISODIC.DEFAULT_OFFSET } = req.query;
|
||||||
const sessions = episodic.getSessions(Number(limit), Number(offset));
|
const sessions = episodic.getSessions(Number(limit), Number(offset));
|
||||||
res.json(sessions);
|
res.json(sessions);
|
||||||
})
|
})
|
||||||
@@ -88,7 +89,7 @@ app.post('/episodes', async (req, res) => {
|
|||||||
|
|
||||||
// Search MUST come before /:id — otherwise 'search' gets captured as an id
|
// Search MUST come before /:id — otherwise 'search' gets captured as an id
|
||||||
app.get('/episodes/search', (req, res) => {
|
app.get('/episodes/search', (req, res) => {
|
||||||
const { q, limit = 10 } = req.query;
|
const { q, limit = EPISODIC.DEFAULT_PAGE_SIZE } = req.query;
|
||||||
if (!q) return res.status(400).json({ error: 'q (query) parameter is required' });
|
if (!q) return res.status(400).json({ error: 'q (query) parameter is required' });
|
||||||
const results = episodic.searchEpisodes(q, Number(limit));
|
const results = episodic.searchEpisodes(q, Number(limit));
|
||||||
res.json(results);
|
res.json(results);
|
||||||
@@ -102,7 +103,7 @@ app.get('/episodes/:id', (req, res) => {
|
|||||||
|
|
||||||
// Get paginated episodes for a session
|
// Get paginated episodes for a session
|
||||||
app.get('/sessions/:id/episodes', (req, res) => {
|
app.get('/sessions/:id/episodes', (req, res) => {
|
||||||
const { limit = 10, offset = 0 } = req.query;
|
const { limit = EPISODIC.DEFAULT_PAGE_SIZE, offset = EPISODIC.DEFAULT_OFFSET } = req.query;
|
||||||
const episodes = episodic.getEpisodesBySession(
|
const episodes = episodic.getEpisodesBySession(
|
||||||
req.params.id,
|
req.params.id,
|
||||||
Number(limit),
|
Number(limit),
|
||||||
|
|||||||
@@ -2,14 +2,9 @@ const memory = require('../services/memory');
|
|||||||
const inference = require('../services/inference');
|
const inference = require('../services/inference');
|
||||||
const embedding = require('../services/embedding');
|
const embedding = require('../services/embedding');
|
||||||
const qdrant = require('../services/qdrant');
|
const qdrant = require('../services/qdrant');
|
||||||
|
const { ORCHESTRATION } = require('@nexusai/shared')
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `You are a helpful, context-aware AI assistant.
|
const { RECENT_EPISODE_LIMIT, SEMANTIC_LIMIT, SCORE_THRESHOLD, SYSTEM_PROMPT } = ORCHESTRATION;
|
||||||
You have access to memories of past conversations with the user.
|
|
||||||
Use them to provide consistent, personalised responses.`;
|
|
||||||
|
|
||||||
const RECENT_EPISODE_LIMIT = 5; // Number of recent episodes to retrieve for context
|
|
||||||
const SEMANTIC_LIMIT = 5;
|
|
||||||
const SCORE_THRESHOLD = 0.75;
|
|
||||||
|
|
||||||
function buildPrompt(recentEpisodes, semanticEpisodes, userMessage) {
|
function buildPrompt(recentEpisodes, semanticEpisodes, userMessage) {
|
||||||
const parts = [SYSTEM_PROMPT];
|
const parts = [SYSTEM_PROMPT];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require ('dotenv').config();
|
require ('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const {getEnv} = require('@nexusai/shared');
|
const {getEnv, PORTS, SERVICES, ORCHESTRATION} = require('@nexusai/shared');
|
||||||
const chatRouter = require('./routes/chat');
|
const chatRouter = require('./routes/chat');
|
||||||
const sessionsRouter = require('./routes/sessions');
|
const sessionsRouter = require('./routes/sessions');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
@@ -8,25 +8,28 @@ const cors = require('cors');
|
|||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
const PORT = getEnv('PORT', '4000'); // Default to 4000 if PORT is not set
|
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: [
|
origin: [
|
||||||
getEnv('CORS_ORIGIN', 'http://localhost:5173'),
|
getEnv('CORS_ORIGIN', ORCHESTRATION.CORS_ORIGIN),
|
||||||
'http://localhost:5173',
|
ORCHESTRATION.CORS_ORIGIN,
|
||||||
],
|
],
|
||||||
methods: ['GET', 'POST', 'DELETE'],
|
methods: ['GET', 'POST', 'DELETE'],
|
||||||
allowedHeaders: ['Content-Type'],
|
allowedHeaders: ['Content-Type'],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const PORT = getEnv('PORT', PORTS.ORCHESTRATION);
|
||||||
|
const MEMORY_URL = getEnv('MEMORY_SERVICE_URL', SERVICES.MEMORY_URL);
|
||||||
|
const EMBEDDING_URL = getEnv('EMBEDDING_SERVICE_URL', SERVICES.EMBEDDING_URL);
|
||||||
|
const INFERENCE_URL = getEnv('INFERENCE_SERVICE_URL', SERVICES.INFERENCE_URL);
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
service: 'Orchestration Service',
|
service: 'Orchestration Service',
|
||||||
status: 'healthy',
|
status: 'healthy',
|
||||||
memoryService: getEnv('MEMORY_SERVICE_URL', 'http://localhost:3002'),
|
memoryService: MEMORY_URL,
|
||||||
embeddingService: getEnv('EMBEDDING_SERVICE_URL', 'http://localhost:3003'),
|
embeddingService: EMBEDDING_URL,
|
||||||
inferenceService: getEnv('INFERENCE_SERVICE_URL', 'http://localhost:3001'),
|
inferenceService: INFERENCE_URL,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
const { Router } = require('express');
|
const { Router } = require('express');
|
||||||
const memory = require('../services/memory');
|
const memory = require('../services/memory');
|
||||||
|
const { EPISODIC } = require('@nexusai/shared');
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/:sessionId/history', async (req, res) => {
|
router.get('/:sessionId/history', async (req, res) => {
|
||||||
const { sessionId } = req.params;
|
const { sessionId } = req.params;
|
||||||
const { limit = 20, offset = 0 } = req.query;
|
const { limit = EPISODIC.DEFAULT_PAGE_SIZE, offset = EPISODIC.DEFAULT_OFFSET } = req.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await memory.getSessionByExternalId(sessionId);
|
const session = await memory.getSessionByExternalId(sessionId);
|
||||||
@@ -19,7 +20,7 @@ router.get('/:sessionId/history', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const {limit = 20, offset = 0 } = req.query;
|
const { limit = EPISODIC.DEFAULT_PAGE_SIZE, offset = EPISODIC.DEFAULT_OFFSET } = req.query;
|
||||||
try {
|
try {
|
||||||
const sessions = await memory.getSessions(Number(limit), Number(offset));
|
const sessions = await memory.getSessions(Number(limit), Number(offset));
|
||||||
res.json(sessions);
|
res.json(sessions);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const fetch = require('node-fetch');
|
const { getEnv, SERVICES } = require('@nexusai/shared');
|
||||||
const { getEnv } = require('@nexusai/shared');
|
|
||||||
|
|
||||||
const BASE_URL = getEnv('INFERENCE_SERVICE_URL', 'http://localhost:3001');
|
const BASE_URL = getEnv('INFERENCE_SERVICE_URL', SERVICES.INFERENCE_URL);
|
||||||
|
|
||||||
async function complete(prompt, options ={}) {
|
async function complete(prompt, options ={}) {
|
||||||
const res = await fetch(`${BASE_URL}/complete`, {
|
const res = await fetch(`${BASE_URL}/complete`, {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const fetch = require('node-fetch');
|
const { getEnv, SERVICES, EPISODIC } = require('@nexusai/shared');
|
||||||
const { getEnv } = require('@nexusai/shared');
|
|
||||||
|
|
||||||
const BASE_URL = getEnv('MEMORY_SERVICE_URL', 'http://localhost:3002');
|
const BASE_URL = getEnv('MEMORY_SERVICE_URL', SERVICES.MEMORY_URL);
|
||||||
|
|
||||||
//function to get session by external id, returns null if not found, throws error for other issues
|
//function to get session by external id, returns null if not found, throws error for other issues
|
||||||
async function getSessionByExternalId(externalId) {
|
async function getSessionByExternalId(externalId) {
|
||||||
@@ -24,7 +23,7 @@ async function createSession(externalId) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRecentEpisodes(sessionId, limit = 10) {
|
async function getRecentEpisodes(sessionId, limit = EPISODIC.DEFAULT_SESSIONS_LIMIT) {
|
||||||
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/episodes?limit=${limit}`);
|
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/episodes?limit=${limit}`);
|
||||||
if (!res.ok) throw new Error(`Failed to fetch episodes: ${res.status} ${res.statusText}`);
|
if (!res.ok) throw new Error(`Failed to fetch episodes: ${res.status} ${res.statusText}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
@@ -47,7 +46,7 @@ async function getEpisodeById(episodeId) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSessionHistory(sessionId, limit = 20, offset = 0) {
|
async function getSessionHistory(sessionId, limit = EPISODIC.DEFAULT_SESSIONS_LIMIT, offset = EPISODIC.DEFAULT_OFFSET) {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${BASE_URL}/sessions/${sessionId}/episodes?limit=${limit}&offset=${offset}`
|
`${BASE_URL}/sessions/${sessionId}/episodes?limit=${limit}&offset=${offset}`
|
||||||
);
|
);
|
||||||
@@ -56,9 +55,9 @@ async function getSessionHistory(sessionId, limit = 20, offset = 0) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSessions(limit = 20, offset = 0) {
|
async function getSessions(limit = EPISODIC.DEFAULT_SESSIONS_LIMIT, offset = EPISODIC.DEFAULT_OFFSET) {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${BASE_URL}/sessions?limit${limit}&offset=${offset}`
|
`${BASE_URL}/sessions?limit=${limit}&offset=${offset}`
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error(`Failed to fetch sessions: ${res.status}`);
|
if (!res.ok) throw new Error(`Failed to fetch sessions: ${res.status}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const {getEnv, QDRANT, COLLECTIONS } = require('@nexusai/shared')
|
const {getEnv, QDRANT, COLLECTIONS, ORCHESTRATION } = require('@nexusai/shared')
|
||||||
|
|
||||||
const BASE_URL = getEnv('QDRANT_URL', QDRANT.DEFAULT_URL);
|
const BASE_URL = getEnv('QDRANT_URL', QDRANT.DEFAULT_URL);
|
||||||
|
|
||||||
async function searchEpisodes( vector, {limit = 5, scoreThreshold = 0.75, sessionId } = {}) {
|
async function searchEpisodes( vector, {limit = ORCHESTRATION.RECENT_EPISODE_LIMIT, scoreThreshold = ORCHESTRATION.SCORE_THRESHOLD, sessionId } = {}) {
|
||||||
const body = {vector, limit, score_threshold: scoreThreshold, with_payload: true};
|
const body = {vector, limit, score_threshold: scoreThreshold, with_payload: true};
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
|
|||||||
@@ -17,15 +17,64 @@ const EPISODIC = {
|
|||||||
DEFAULT_RECENT_LIMIT: 10, // Default number of recent episodes to retrieve
|
DEFAULT_RECENT_LIMIT: 10, // Default number of recent episodes to retrieve
|
||||||
DEFAULT_PAGE_SIZE: 20, // Default number of episodes per page for pagination
|
DEFAULT_PAGE_SIZE: 20, // Default number of episodes per page for pagination
|
||||||
DEFAULT_SEARCH_LIMIT: 10, // Default number of search results to return
|
DEFAULT_SEARCH_LIMIT: 10, // Default number of search results to return
|
||||||
|
DEFAULT_OFFSET: 0,
|
||||||
|
DEFAULT_SESSIONS_LIMIT: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ORCHESTRATION = {
|
||||||
|
RECENT_EPISODE_LIMIT: 5,
|
||||||
|
SEMANTIC_LIMIT: 5,
|
||||||
|
SCORE_THRESHOLD: 0.75,
|
||||||
|
CORS_ORIGIN: 'http://localhost:5173',
|
||||||
|
SYSTEM_PROMPT: `You are a helpful, context-aware AI assistant. You have access to memories of past conversations with the user. Use them to provide consistent, personalised responses.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const OLLAMA = {
|
||||||
|
DEFAULT_URL: 'http://localhost:11434',
|
||||||
|
EMBED_MODEL: 'nomic-embed-text',
|
||||||
|
OLLAMA_MODEL: 'companion:latest',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LLAMACPP = {
|
||||||
|
DEFAULT_URL: 'http://localhost:8080',
|
||||||
|
DEFAULT_MODEL: 'local-model',
|
||||||
|
}
|
||||||
|
|
||||||
|
const PORTS = {
|
||||||
|
INFERENCE: '3001',
|
||||||
|
MEMORY: '3002',
|
||||||
|
EMBEDDING: '3003',
|
||||||
|
ORCHESTRATION: '4000',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SERVICES = {
|
const SERVICES = {
|
||||||
EMBEDDING_URL: 'http://localhost:3003'
|
EMBEDDING_URL: `http://localhost:${PORTS.EMBEDDING}`,
|
||||||
|
MEMORY_URL: `http://localhost:${PORTS.MEMORY}`,
|
||||||
|
INFERENCE_URL: `http://localhost:${PORTS.INFERENCE}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INFERENCE_DEFAULTS = {
|
||||||
|
TEMPERATURE: 0.7, // Controls randomness. 0 = deterministic, 1 = creative
|
||||||
|
MAX_TOKENS: 1024, // Max tokens to generate in a response
|
||||||
|
TOP_P: 0.9, // Nucleus sampling — considers tokens comprising top 90% probability mass
|
||||||
|
TOP_K: 40, // Limits token selection to top K candidates at each step
|
||||||
|
REPEAT_PENALTY: 1.1, // Penalizes recently used tokens to reduce repetition
|
||||||
|
SEED: null, // null = random. Set to an integer for reproducible outputs
|
||||||
|
};
|
||||||
|
|
||||||
|
const SQLITE = {
|
||||||
|
DEFAULT_PATH: './data/nexusai.db'
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
QDRANT,
|
QDRANT,
|
||||||
COLLECTIONS,
|
COLLECTIONS,
|
||||||
EPISODIC,
|
EPISODIC,
|
||||||
SERVICES
|
SERVICES,
|
||||||
|
OLLAMA,
|
||||||
|
PORTS,
|
||||||
|
LLAMACPP,
|
||||||
|
INFERENCE_DEFAULTS,
|
||||||
|
SQLITE,
|
||||||
|
ORCHESTRATION
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,19 @@
|
|||||||
const {getEnv} = require('./config/env');
|
const {getEnv} = require('./config/env');
|
||||||
const {QDRANT, COLLECTIONS, EPISODIC, SERVICES } = require('./config/constants');
|
const {QDRANT, COLLECTIONS, EPISODIC, SERVICES, OLLAMA, PORTS, LLAMACPP, INFERENCE_DEFAULTS, SQLITE, ORCHESTRATION } = require('./config/constants');
|
||||||
|
const {parseRow, formatEpisodeText} = require('./utils')
|
||||||
|
|
||||||
module.exports = {getEnv, QDRANT, COLLECTIONS, EPISODIC, SERVICES};
|
module.exports = {
|
||||||
|
getEnv,
|
||||||
|
QDRANT,
|
||||||
|
COLLECTIONS,
|
||||||
|
EPISODIC,
|
||||||
|
SERVICES,
|
||||||
|
OLLAMA,
|
||||||
|
PORTS,
|
||||||
|
LLAMACPP,
|
||||||
|
INFERENCE_DEFAULTS,
|
||||||
|
SQLITE,
|
||||||
|
ORCHESTRATION,
|
||||||
|
parseRow,
|
||||||
|
formatEpisodeText,
|
||||||
|
};
|
||||||
13
packages/shared/src/utils.js
Normal file
13
packages/shared/src/utils.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
function parseRow(row) {
|
||||||
|
if (!row) return null;
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEpisodeText(userMessage, aiResponse) {
|
||||||
|
return `User: ${userMessage}\nAssistant: ${aiResponse}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parseRow, formatEpisodeText };
|
||||||
BIN
packages/source.zip
Normal file
BIN
packages/source.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user