project summaries addition
This commit is contained in:
@@ -50,4 +50,27 @@ function deleteSummary(id) {
|
|||||||
getDB().prepare(`DELETE FROM summaries WHERE id = ?`).run(id);
|
getDB().prepare(`DELETE FROM summaries WHERE id = ?`).run(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createSummary, getSummary, getSummariesBySession, getSummariesByProject, updateSummary, deleteSummary };
|
// 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 };
|
||||||
@@ -4,6 +4,7 @@ const {getEnv, PORTS, EPISODIC} = require('@nexusai/shared');
|
|||||||
const { getDB } = require('./db');
|
const { getDB } = require('./db');
|
||||||
const { createProject, getProjects, getProject, updateProject, deleteProject } = require('./db/projects');
|
const { createProject, getProjects, getProject, updateProject, deleteProject } = require('./db/projects');
|
||||||
const { createSummary, getSummary, getSummariesBySession, getSummariesByProject, updateSummary, deleteSummary } = require('./db/summaries');
|
const { createSummary, getSummary, getSummariesBySession, getSummariesByProject, updateSummary, deleteSummary } = require('./db/summaries');
|
||||||
|
const { generateAndStoreProjectSummary } = require('./summarization/project');
|
||||||
|
|
||||||
const episodic = require('./episodic');
|
const episodic = require('./episodic');
|
||||||
const semantic = require('./semantic');
|
const semantic = require('./semantic');
|
||||||
@@ -242,6 +243,36 @@ app.get('/projects', (req, res) => {
|
|||||||
res.json(getProjects());
|
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) => {
|
app.get('/projects/:id', (req, res) => {
|
||||||
const project = getProject(req.params.id);
|
const project = getProject(req.params.id);
|
||||||
if (!project) return res.status(404).json({ error: 'Not found' });
|
if (!project) return res.status(404).json({ error: 'Not found' });
|
||||||
@@ -262,6 +293,10 @@ app.delete('/projects/:id', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************** */
|
/*********************************** */
|
||||||
/********** Summary Routes ********** */
|
/********** Summary Routes ********** */
|
||||||
/*********************************** */
|
/*********************************** */
|
||||||
@@ -285,11 +320,6 @@ app.get('/sessions/:id/summaries', (req, res) => {
|
|||||||
res.json(getSummariesBySession(req.params.id));
|
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)
|
// Update a summary (for cumulative updates)
|
||||||
app.patch('/summaries/:id', (req, res) => {
|
app.patch('/summaries/:id', (req, res) => {
|
||||||
const summary = getSummary(req.params.id);
|
const summary = getSummary(req.params.id);
|
||||||
|
|||||||
81
packages/memory-service/src/summarization/project.js
Normal file
81
packages/memory-service/src/summarization/project.js
Normal file
@@ -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 };
|
||||||
@@ -3,6 +3,28 @@ const memory = require('../services/memory');
|
|||||||
|
|
||||||
const router = Router();
|
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) => {
|
router.get('/session/:sessionId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const session = await memory.getSessionByExternalId(req.params.sessionId);
|
const session = await memory.getSessionByExternalId(req.params.sessionId);
|
||||||
|
|||||||
@@ -181,7 +181,20 @@ async function getSummariesByProject(projectId) {
|
|||||||
if (!res.ok) throw new Error(`Failed to fetch summaries: ${res.status}`);
|
if (!res.ok) throw new Error(`Failed to fetch summaries: ${res.status}`);
|
||||||
return res.json();
|
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 = {
|
module.exports = {
|
||||||
getSessionByExternalId,
|
getSessionByExternalId,
|
||||||
@@ -205,4 +218,6 @@ module.exports = {
|
|||||||
createSummary,
|
createSummary,
|
||||||
updateSummary,
|
updateSummary,
|
||||||
getSummariesByProject,
|
getSummariesByProject,
|
||||||
|
generateProjectSummary,
|
||||||
|
getProjectOverviewSummary,
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user