Files
nexusAI/packages/memory-service/src/summarization/project.js
2026-04-26 19:11:40 -07:00

82 lines
3.2 KiB
JavaScript

const { SERVICES, getEnv } = require('@nexusai/shared');
const {
getSessionSummariesForProject,
getProjectOverviewSummary,
createSummary,
updateSummary,
} = require('../db/summaries');
const { getProject } = require('../db/projects');
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 };