roadmap phase 1 complete

This commit is contained in:
Storme-bit
2026-04-27 03:10:39 -07:00
parent 9fe8e568cf
commit 1a97b19280
19 changed files with 759 additions and 281 deletions
@@ -5,22 +5,20 @@ const qdrant = require("../services/qdrant");
const { ORCHESTRATION, logger } = require("@nexusai/shared");
const appSettings = require("../config/settings");
const {triggerSummary} = require('../services/summarization')
const graph = require('../services/graph');
function buildPrompt(recentEpisodes, semanticEpisodes, entities, userMessage, systemPrompt) {
function buildPrompt(recentEpisodes, semanticEpisodes, neighborhood, userMessage, systemPrompt) {
const parts = [systemPrompt ?? ORCHESTRATION.SYSTEM_PROMPT];
if (entities.length > 0) {
parts.push(
"Here is what you know about entities relevant to this conversation:",
);
for (const e of entities) {
parts.push(`- ${e.name} (${e.type}): ${e.notes}`);
const graphText = formatGraphContext(neighborhood.nodes ?? [], neighborhood.edges ?? []);
if (graphText) {
parts.push("Here is what you know about entities relevant to this conversation and their connections:");
parts.push(graphText);
parts.push("---");
}
parts.push("---");
}
if (semanticEpisodes.length > 0) {
parts.push("Here are some relevant memories from earlier conversations:");
parts.push("Long-term memory (semantically relevant to this message):");
for (const ep of semanticEpisodes) {
parts.push(`User: ${ep.user_message}\nAssistant: ${ep.ai_response}`);
}
@@ -28,7 +26,7 @@ function buildPrompt(recentEpisodes, semanticEpisodes, entities, userMessage, sy
}
if (recentEpisodes.length > 0) {
parts.push(`Here are some relevant memories from your past conversations:`);
parts.push("Recent conversation history (most recent exchanges):");
for (const ep of recentEpisodes) {
parts.push(`User: ${ep.user_message}\nAssistant: ${ep.ai_response}`);
}
@@ -54,6 +52,28 @@ function buildNamingPrompt(userMessage, aiResponse) {
].join("\n");
}
function formatGraphContext(nodes, edges) {
if (!nodes.length) return null;
const nodeMap = new Map(nodes.map(n => [n.id, n]));
// Build outbound adjacency
const outbound = new Map(nodes.map(n => [n.id, []]));
for (const edge of edges) {
if (outbound.has(edge.from_id) && nodeMap.has(edge.to_id)) {
const target = nodeMap.get(edge.to_id);
outbound.get(edge.from_id).push(`${edge.label} ${target.name} (${target.type})`);
}
}
return nodes.map(n => {
const lines = [`- ${n.name} (${n.type}): ${n.notes ?? '(no notes)'}`];
for (const conn of outbound.get(n.id) ?? []) lines.push(` → ${conn}`);
return lines.join('\n');
}).join('\n');
}
async function autoNameSession(externalId, userMessage, aiResponse) {
try {
const prompt = buildNamingPrompt(userMessage, aiResponse);
@@ -107,22 +127,20 @@ async function getSemanticEpisodes(
}
}
async function getRelevantEntities(userMessage, projectId=null) {
try {
const vector = await embedding.embed(userMessage);
const results = await qdrant.searchEntities(vector, { projectId });
logger.info(
"[orchestration] Entity search results:",
results.map((r) => ({ name: r.payload?.name, score: r.score })),
);
return results.map((r) => r.payload).filter(Boolean);
} catch (err) {
logger.debug(
"[orchestration] Entity search failed, continuing without:",
err.message,
);
return [];
}
async function getRelevantEntities(userMessage, projectId = null) {
try {
const vector = await embedding.embed(userMessage);
const results = await qdrant.searchEntities(vector, { projectId });
logger.info(
'[orchestration] Entity search results:',
results.map((r) => ({ name: r.payload?.name, score: r.score })),
);
// Include the Qdrant point ID (== SQLite entity ID) for graph traversal
return results.map((r) => r.payload ? { id: r.id, ...r.payload } : null).filter(Boolean);
} catch (err) {
logger.debug('[orchestration] Entity search failed, continuing without:', err.message);
return [];
}
}
async function assembleContext(externalId, userMessage) {
@@ -159,10 +177,22 @@ async function assembleContext(externalId, userMessage) {
const semanticEpisodes = await getSemanticEpisodes(
userMessage, session.id, recentIds, projectSessionIds, { semanticLimit, scoreThreshold }
);
const entities = await getRelevantEntities(userMessage, session.project_id ?? null);
const entityResults = await getRelevantEntities(userMessage, session.project_id ?? null);
// 5. Assemble prompt
const prompt = buildPrompt(recentEpisodes, semanticEpisodes, entities, userMessage, activeSystemPrompt);
// 5. Expand matched entities into 1-hop graph neighborhood
let neighborhood = { nodes: [], edges: [] };
if (entityResults.length > 0) {
try {
neighborhood = await graph.getNeighbors(entityResults.map(e => e.id));
} catch (err) {
logger.warn('[orchestration] Graph neighborhood fetch failed, falling back to flat entities:', err.message);
// Graceful fallback: use Qdrant payload data as flat nodes, no edges
neighborhood = { nodes: entityResults, edges: [] };
}
}
// 6. Assemble prompt
const prompt = buildPrompt(recentEpisodes, semanticEpisodes, neighborhood, userMessage, activeSystemPrompt);
return {
session,
@@ -0,0 +1,15 @@
const { getEnv, SERVICES } = require('@nexusai/shared');
const MEMORY_URL = getEnv('MEMORY_SERVICE_URL', SERVICES.MEMORY_URL);
async function getNeighbors(entityIds) {
const res = await fetch(`${MEMORY_URL}/graph/neighbors`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ entityIds }),
});
if (!res.ok) throw new Error(`Graph neighbors error: ${res.status}`);
return res.json();
}
module.exports = { getNeighbors };