diff --git a/packages/memory-service/src/entities/extraction.js b/packages/memory-service/src/entities/extraction.js index 499dee8..e89d9bd 100644 --- a/packages/memory-service/src/entities/extraction.js +++ b/packages/memory-service/src/entities/extraction.js @@ -64,7 +64,7 @@ async function embedEntity(entity) { return data.embedding; } -async function extractAndStoreEntities(userMessage, aiResponse) { +async function extractAndStoreEntities(userMessage, aiResponse, projectId=null) { console.log('[entities] Extraction triggered') try { // Fetch existing entities to guide the model toward consistent name/type pairs @@ -109,6 +109,7 @@ async function extractAndStoreEntities(userMessage, aiResponse) { name: entity.name, type: entity.type, notes: entity.notes, + projectId: projectId ?? null, })) .catch(err => { console.warn(`[entities] Failed to embed entity "${entity.name}":`, err.message); diff --git a/packages/memory-service/src/episodic/index.js b/packages/memory-service/src/episodic/index.js index c44e102..03f210e 100644 --- a/packages/memory-service/src/episodic/index.js +++ b/packages/memory-service/src/episodic/index.js @@ -98,7 +98,7 @@ function deleteSessionByExternalId(externalId) { // --Episodes -------------------------------------------------- // Creates a new episode linked to a session, with user message, AI response, optional token count, and metadata -async function createEpisode(sessionId, userMessage, aiResponse, tokenCount = null, metadata = null) { +async function createEpisode(sessionId, userMessage, aiResponse, tokenCount = null, metadata = null, projectId=null) { const db = getDB(); // Wrap insert + session touch in a transaction — both succeed or neither does @@ -128,7 +128,7 @@ async function createEpisode(sessionId, userMessage, aiResponse, tokenCount = nu })) .catch(err => console.error(`Failed to embed episode ${episode.id}:`, err.message)); - extractAndStoreEntities(userMessage, aiResponse) + extractAndStoreEntities(userMessage, aiResponse, projectId) .catch(err => console.error(`Failed to extract entities for episode ${episode.id}:`, err.message)); diff --git a/packages/memory-service/src/index.js b/packages/memory-service/src/index.js index f011962..67e19e9 100644 --- a/packages/memory-service/src/index.js +++ b/packages/memory-service/src/index.js @@ -96,11 +96,11 @@ app.delete('/sessions/by-external/:externalId', (req, res) => { /************************************* */ app.post('/episodes', async (req, res) => { - const { sessionId, userMessage, aiResponse, tokenCount, metadata } = req.body; + const { sessionId, userMessage, aiResponse, tokenCount, metadata, projectId } = 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); + const episode = await episodic.createEpisode(sessionId, userMessage, aiResponse, tokenCount, metadata, projectId); console.log('[memory] create episode body:', { sessionId, diff --git a/packages/orchestration-service/src/chat/index.js b/packages/orchestration-service/src/chat/index.js index 14df81b..fd4bd5d 100644 --- a/packages/orchestration-service/src/chat/index.js +++ b/packages/orchestration-service/src/chat/index.js @@ -107,10 +107,10 @@ async function getSemanticEpisodes( } } -async function getRelevantEntities(userMessage) { +async function getRelevantEntities(userMessage, projectId=null) { try { const vector = await embedding.embed(userMessage); - const results = await qdrant.searchEntities(vector); + const results = await qdrant.searchEntities(vector, { projectId }); console.log( "[orchestration] Entity search results:", results.map((r) => ({ name: r.payload?.name, score: r.score })), @@ -176,7 +176,7 @@ async function chat(externalId, userMessage, options = {}) { ); // 3b. Entity Search - const entities = await getRelevantEntities(userMessage); + const entities = await getRelevantEntities(userMessage, session.project_id ?? null); // 4. Assemble prompt const prompt = buildPrompt( @@ -196,6 +196,7 @@ async function chat(externalId, userMessage, options = {}) { userMessage, result.text, (result.evalCount || 0) + (result.promptEvalCount || 0), + session.project_id ?? null, ) .catch((err) => console.error(`[orchestration] Failed to save episode`, err.message), @@ -262,7 +263,7 @@ async function chatStream(externalId, userMessage, onChunk, options = {}) { {semanticLimit, scoreThreshold } ); - const entities = await getRelevantEntities(userMessage); + const entities = await getRelevantEntities(userMessage, session.project_id ?? null); const prompt = buildPrompt( recentEpisodes, @@ -323,7 +324,7 @@ async function chatStream(externalId, userMessage, onChunk, options = {}) { console.log("[orchestration] final streamed text length:", fullText.length); if (fullText.trim()) { - await memory.createEpisode(session.id, userMessage, fullText, tokenCount); + await memory.createEpisode(session.id, userMessage, fullText, tokenCount, session.project_id ?? null); } else { console.warn( "[orchestration] Stream finished with no assistant text; episode not saved", diff --git a/packages/orchestration-service/src/services/memory.js b/packages/orchestration-service/src/services/memory.js index 9e441cc..dc2697b 100644 --- a/packages/orchestration-service/src/services/memory.js +++ b/packages/orchestration-service/src/services/memory.js @@ -29,11 +29,11 @@ async function getRecentEpisodes(sessionId, limit = EPISODIC.DEFAULT_SESSIONS_LI return res.json(); } -async function createEpisode(sessionId, userMessage, aiResponse, tokenCount) { +async function createEpisode(sessionId, userMessage, aiResponse, tokenCount, projectId=null) { const res = await fetch(`${BASE_URL}/episodes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionId, userMessage, aiResponse, tokenCount }) + body: JSON.stringify({ sessionId, userMessage, aiResponse, tokenCount, projectId }) }); if (!res.ok) throw new Error(`Failed to create episode: ${res.status} ${res.statusText}`); return res.json(); diff --git a/packages/orchestration-service/src/services/qdrant.js b/packages/orchestration-service/src/services/qdrant.js index fa0444f..38a1657 100644 --- a/packages/orchestration-service/src/services/qdrant.js +++ b/packages/orchestration-service/src/services/qdrant.js @@ -33,9 +33,15 @@ async function searchEpisodes( vector, {limit = ORCHESTRATION.RECENT_EPISODE_LIM return data.result; } -async function searchEntities(vector, { limit = 5, scoreThreshold = 0.6 } = {}) { +async function searchEntities(vector, { limit = ORCHESTRATION.ENTITIES_LIMIT, scoreThreshold = ORCHESTRATION.ENTITIES_THRESHOLD, projectId = undefined } = {}) { const body = { vector, limit, score_threshold: scoreThreshold, with_payload: true }; + if (projectId !== undefined) { + body.filter = { + must: [{ key: 'projectId', match: { value: projectId ?? null } }] + }; + } + const res = await fetch( `${BASE_URL}/collections/${COLLECTIONS.ENTITIES}/points/search`, { diff --git a/packages/shared/src/config/constants.js b/packages/shared/src/config/constants.js index 18c787a..0076869 100644 --- a/packages/shared/src/config/constants.js +++ b/packages/shared/src/config/constants.js @@ -25,6 +25,8 @@ const ORCHESTRATION = { RECENT_EPISODE_LIMIT: 5, SEMANTIC_LIMIT: 5, SCORE_THRESHOLD: 0.75, + ENTITIES_LIMIT: 5, + ENTITIES_THRESHOLD: 0.75, TEMPERATURE: 0.7, 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.`