project summaries addition
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user