project summaries addition
This commit is contained in:
@@ -50,4 +50,27 @@ function deleteSummary(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 { 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);
|
||||
|
||||
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 };
|
||||
Reference in New Issue
Block a user