project summaries addition

This commit is contained in:
Storme-bit
2026-04-26 21:02:42 -07:00
parent fcaf0e651f
commit 855de6d0af
7 changed files with 190 additions and 36 deletions

View File

@@ -207,6 +207,17 @@ async function getEpisodeEmbedding(userMessage, aiResponse){
return data.embedding;
}
function getEpisodesByProject(projectId, limit = 200) {
const db = getDB();
return db.prepare(`
SELECT e.* FROM episodes e
JOIN sessions s ON s.id = e.session_id
WHERE s.project_id = ?
ORDER BY e.created_at ASC
LIMIT ?
`).all(projectId, limit).map(parseRow);
}
module.exports = {
createSession,
getSession,
@@ -221,5 +232,6 @@ module.exports = {
getEpisodesBySession,
getRecentEpisodes,
searchEpisodes,
deleteEpisode
deleteEpisode,
getEpisodesByProject
};

View File

@@ -252,8 +252,7 @@ app.post('/projects/:id/summarize', async (req, res) => {
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')) {
if (err.message.includes('No session summaries or episodes')) {
return res.status(422).json({ error: err.message });
}
res.status(500).json({ error: err.message });

View File

@@ -6,6 +6,7 @@ const {
updateSummary,
} = require('../db/summaries');
const { getEpisodesByProject } = require('../episodic');
const { getProject } = require('../db/projects');
const EXTRACTION_URL = getEnv('EXTRACTION_URL', 'http://localhost:11434');
@@ -34,6 +35,55 @@ function buildProjectSummaryPrompt(projectName, sessionSummaries) {
].join('\n');
}
function buildProjectSummaryFromEpisodesPrompt(projectName, episodes) {
// Condense episodes into a readable block, truncating if needed
let episodeBlock = episodes
.map(ep => `User: ${ep.user_message}\nAssistant: ${ep.ai_response}`)
.join('\n\n');
if (episodeBlock.length > MAX_SUMMARY_CHARS) {
// Keep the most recent episodes — slice from the end
episodeBlock = episodeBlock.slice(-MAX_SUMMARY_CHARS);
}
return [
'<|im_start|>user',
`The following are conversations 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.',
'',
episodeBlock,
'<|im_end|>',
'<|im_start|>assistant',
].join('\n');
}
async function generateProjectSummaryFromEpisodes(projectName, episodes) {
const prompt = buildProjectSummaryFromEpisodesPrompt(projectName, episodes);
const res = await fetch(`${EXTRACTION_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: EXTRACTION_MODEL,
prompt,
stream: false,
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();
}
async function generateProjectSummary(projectName, sessionSummaries) {
const prompt = buildProjectSummaryPrompt(projectName, sessionSummaries);
@@ -64,13 +114,23 @@ async function generateAndStoreProjectSummary(projectId) {
const project = getProject(projectId);
if (!project) throw new Error('Project not found');
let content;
const sessionSummaries = getSessionSummariesForProject(projectId);
if (!sessionSummaries.length) throw new Error('No session summaries available to summarize');
const content = await generateProjectSummary(project.name, sessionSummaries);
if (sessionSummaries.length > 0) {
// Preferred path — summarize the summaries
content = await generateProjectSummary(project.name, sessionSummaries);
} else {
// Fallback — summarize raw episodes directly
const episodes = getEpisodesByProject(projectId);
if (!episodes.length) {
throw new Error('No session summaries or episodes found for this project');
}
content = await generateProjectSummaryFromEpisodes(project.name, episodes);
}
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 });