From 4c6bd1df2dc70a9a1fa97df7e7bd88cb50df1857 Mon Sep 17 00:00:00 2001 From: Storme-bit Date: Sun, 26 Apr 2026 18:57:25 -0700 Subject: [PATCH] project summaries addition --- packages/memory-service/src/db/summaries.js | 25 +++++- packages/memory-service/src/index.js | 40 +++++++-- .../src/summarization/project.js | 81 +++++++++++++++++++ .../src/routes/summaries.js | 22 +++++ .../src/services/memory.js | 17 +++- 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 packages/memory-service/src/summarization/project.js diff --git a/packages/memory-service/src/db/summaries.js b/packages/memory-service/src/db/summaries.js index 5a4bb1f..630f063 100644 --- a/packages/memory-service/src/db/summaries.js +++ b/packages/memory-service/src/db/summaries.js @@ -50,4 +50,27 @@ function deleteSummary(id) { getDB().prepare(`DELETE FROM summaries WHERE id = ?`).run(id); } -module.exports = { createSummary, getSummary, getSummariesBySession, getSummariesByProject, updateSummary, deleteSummary }; \ No newline at end of file +// Fetches session summaries that belong to sessions in a given project +// Joins through sessions table since session summaries don't store project_id directly +function getSessionSummariesForProject(projectId) { + const db = getDB(); + return db.prepare(` + SELECT s.* FROM summaries s + JOIN sessions sess ON sess.id = s.session_id + WHERE sess.project_id = ? AND s.session_id IS NOT NULL + ORDER BY s.created_at ASC + `).all(projectId).map(parseRow); +} + +// Fetches the most recent project-level overview summary (session_id IS NULL distinguishes it) +function getProjectOverviewSummary(projectId) { + const db = getDB(); + const row = db.prepare(` + SELECT * FROM summaries + WHERE project_id = ? AND session_id IS NULL + ORDER BY created_at DESC LIMIT 1 + `).get(projectId); + return row ? parseRow(row) : null; +} + +module.exports = { createSummary, getSummary, getSummariesBySession, getSummariesByProject, updateSummary, deleteSummary, getSessionSummariesForProject, getProjectOverviewSummary }; \ No newline at end of file diff --git a/packages/memory-service/src/index.js b/packages/memory-service/src/index.js index 6321bab..8a7c3ff 100644 --- a/packages/memory-service/src/index.js +++ b/packages/memory-service/src/index.js @@ -4,6 +4,7 @@ const {getEnv, PORTS, EPISODIC} = require('@nexusai/shared'); const { getDB } = require('./db'); const { createProject, getProjects, getProject, updateProject, deleteProject } = require('./db/projects'); const { createSummary, getSummary, getSummariesBySession, getSummariesByProject, updateSummary, deleteSummary } = require('./db/summaries'); +const { generateAndStoreProjectSummary } = require('./summarization/project'); const episodic = require('./episodic'); const semantic = require('./semantic'); @@ -242,6 +243,36 @@ app.get('/projects', (req, res) => { res.json(getProjects()); }); +// Generate (or regenerate) a project overview summary on demand +app.post('/projects/:id/summarize', async (req, res) => { + const project = getProject(Number(req.params.id)); + if (!project) return res.status(404).json({ error: 'Project not found' }); + + try { + const summary = await generateAndStoreProjectSummary(Number(req.params.id)); + res.status(201).json(summary); + } catch (err) { + // Distinguish "no data" from actual errors — both are non-500 from the client's perspective + if (err.message.includes('No session summaries')) { + return res.status(422).json({ error: err.message }); + } + res.status(500).json({ error: err.message }); + } +}); + +// Get the current project overview summary +app.get('/projects/:id/overview', async (req, res) => { + const { getProjectOverviewSummary } = require('./db/summaries'); + const summary = getProjectOverviewSummary(Number(req.params.id)); + // 200 with null is fine — frontend can handle "no overview yet" gracefully + res.json(summary ?? null); +}); + +// Get summaries for a project +app.get('/projects/:id/summaries', (req, res) => { + res.json(getSummariesByProject(req.params.id)); +}); + app.get('/projects/:id', (req, res) => { const project = getProject(req.params.id); if (!project) return res.status(404).json({ error: 'Not found' }); @@ -262,6 +293,10 @@ app.delete('/projects/:id', (req, res) => { }); + + + + /*********************************** */ /********** Summary Routes ********** */ /*********************************** */ @@ -285,11 +320,6 @@ app.get('/sessions/:id/summaries', (req, res) => { res.json(getSummariesBySession(req.params.id)); }); -// Get summaries for a project -app.get('/projects/:id/summaries', (req, res) => { - res.json(getSummariesByProject(req.params.id)); -}); - // Update a summary (for cumulative updates) app.patch('/summaries/:id', (req, res) => { const summary = getSummary(req.params.id); diff --git a/packages/memory-service/src/summarization/project.js b/packages/memory-service/src/summarization/project.js new file mode 100644 index 0000000..6ac53da --- /dev/null +++ b/packages/memory-service/src/summarization/project.js @@ -0,0 +1,81 @@ +const { SERVICES, getEnv } = require('@nexusai/shared'); +const { + getSessionSummariesForProject, + getProjectOverviewSummary, + createSummary, + updateSummary, + + } = require('./db/summaries'); + +const EXTRACTION_URL = getEnv('EXTRACTION_URL', 'http://localhost:11434'); +const EXTRACTION_MODEL = getEnv('EXTRACTION_MODEL', 'qwen2.5:3b'); + +const MAX_SUMMARY_CHARS = 8000; // generous ceiling before we truncate input + +function buildProjectSummaryPrompt(projectName, sessionSummaries) { + let summaryBlock = sessionSummaries + .map((s, i) => `Session ${i + 1}:\n${s.content}`) + .join('\n\n'); + + // Guard against very large inputs — truncate oldest sessions if needed + if (summaryBlock.length > MAX_SUMMARY_CHARS) { + summaryBlock = summaryBlock.slice(-MAX_SUMMARY_CHARS); + } + + return [ + '<|im_start|>user', + `The following are session summaries from a project called "${projectName}".`, + 'Write a project overview covering: goals, progress, key decisions, and current state.', + 'Scale the length to the material — use multiple paragraphs for complex projects, a few sentences for simple ones.', + 'Be comprehensive but avoid padding. Do not repeat the same point twice.', + 'Write in third person. Output only the overview text, no headings or labels.', + '', + ].join('\n'); +} + +async function generateProjectSummary(projectName, sessionSummaries) { + const prompt = buildProjectSummaryPrompt(projectName, sessionSummaries); + + const res = await fetch(`${EXTRACTION_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: EXTRACTION_MODEL, + prompt, + stream: false, + // No format: 'json' — we want free-text narrative, same as session summarization + options: { temperature: 0.2, num_predict: 1200 }, + }), + }); + + if (!res.ok) throw new Error(`Ollama responded ${res.status}`); + const data = await res.json(); + + const raw = data.response?.trim() ?? ''; + return raw + .replace(/<\|im_start\|>.*?<\|im_end\|>/gs, '') + .replace(/<\|im_start\|>|<\|im_end\|>|<\|im_sep\|>/g, '') + .trim(); +} + +// Main entry point — called by the route handler +async function generateAndStoreProjectSummary(projectId) { + const project = getProject(projectId); + if (!project) throw new Error('Project not found'); + + const sessionSummaries = getSessionSummariesForProject(projectId); + if (!sessionSummaries.length) throw new Error('No session summaries available to summarize'); + + const content = await generateProjectSummary(project.name, sessionSummaries); + if (!content) throw new Error('Model returned empty summary'); + + // Upsert — replace existing overview if present, otherwise create fresh + const existing = getProjectOverviewSummary(projectId); + if (existing) { + return updateSummary(existing.id, { content, tokenCount: null, episodeRange: null }); + } else { + return createSummary({ projectId, content, sessionId: null }); + } +} + +module.exports = { generateAndStoreProjectSummary }; \ No newline at end of file diff --git a/packages/orchestration-service/src/routes/summaries.js b/packages/orchestration-service/src/routes/summaries.js index 09e277c..4c46318 100644 --- a/packages/orchestration-service/src/routes/summaries.js +++ b/packages/orchestration-service/src/routes/summaries.js @@ -3,6 +3,28 @@ const memory = require('../services/memory'); const router = Router(); +// Trigger on-demand project summary generation +router.post('/project/:projectId/generate', async (req, res) => { + try { + const summary = await memory.generateProjectSummary(req.params.projectId); + res.status(201).json(summary); + } catch (err) { + // Pass through 422 from memory-service ("no session summaries yet") + const status = err.message.includes('422') ? 422 : 500; + res.status(status).json({ error: err.message }); + } +}); + +// Get current project overview summary +router.get('/project/:projectId/overview', async (req, res) => { + try { + const summary = await memory.getProjectOverviewSummary(req.params.projectId); + res.json(summary); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + router.get('/session/:sessionId', async (req, res) => { try { const session = await memory.getSessionByExternalId(req.params.sessionId); diff --git a/packages/orchestration-service/src/services/memory.js b/packages/orchestration-service/src/services/memory.js index b581c13..03616a8 100644 --- a/packages/orchestration-service/src/services/memory.js +++ b/packages/orchestration-service/src/services/memory.js @@ -181,7 +181,20 @@ async function getSummariesByProject(projectId) { if (!res.ok) throw new Error(`Failed to fetch summaries: ${res.status}`); return res.json(); } -// add to module.exports too + +async function generateProjectSummary(projectId) { + const res = await fetch(`${BASE_URL}/projects/${projectId}/summarize`, { + method: 'POST', + }); + if (!res.ok) throw new Error(`Failed to generate project summary: ${res.status}`); + return res.json(); +} + +async function getProjectOverviewSummary(projectId) { + const res = await fetch(`${BASE_URL}/projects/${projectId}/overview`); + if (!res.ok) throw new Error(`Failed to fetch project overview: ${res.status}`); + return res.json(); // null if none exists yet +} module.exports = { getSessionByExternalId, @@ -205,4 +218,6 @@ module.exports = { createSummary, updateSummary, getSummariesByProject, + generateProjectSummary, + getProjectOverviewSummary, } \ No newline at end of file