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