project summaries addition

This commit is contained in:
Storme-bit
2026-04-26 18:57:25 -07:00
parent 2429fedb2c
commit 4c6bd1df2d
5 changed files with 178 additions and 7 deletions

View File

@@ -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 };

View File

@@ -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);

View 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 };

View File

@@ -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);

View File

@@ -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,
} }