godcrm/backend/routes/v3/chat/summaryController.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

166 lines
9.8 KiB
JavaScript

/**
* Conversation summaries routes.
*/
import {
dbRun, dbGet, dbAll, isPostgres, safeJsonParse, apiLogger,
success, created, error, badRequest, notFound,
requireAuth, saveStepMessage,
generateSummaryPrompt, executeSimpleAI,
} from './chatShared.js';
const SUMMARY_AGENT_ID = 85543;
const SUMMARY_SYSTEM_PROMPT = `You are a concise conversation summarizer. Produce a structured summary in TODO/checklist format using Markdown.
Format your response EXACTLY like this:
## Summary
**Key decisions:**
- [ ] Decision or action item 1
- [ ] Decision or action item 2
- [x] Completed item (if clearly done in the conversation)
**What was done:**
- Brief bullet point 1
- Brief bullet point 2
**Status:** One sentence about current state.
Keep it brief — max 8-10 bullet points total. Use checkboxes (- [ ] / - [x]) for action items and plain bullets (-) for facts/status.`;
export default function registerSummaryRoutes(router) {
// GET /conversations/:id/summaries - List past summaries
router.get('/conversations/:id/summaries', requireAuth, async (req, res) => {
const conversationId = Number(req.params.id);
if (!conversationId) return badRequest(res, 'Invalid conversation ID');
try {
const summaryMessages = await dbAll(
isPostgres()
? `SELECT id, conversation_id, content, agent_id, model_used, metadata, created_at FROM messages WHERE conversation_id = $1 AND agent_id = $2 AND role = 'assistant' AND metadata::text LIKE '%"is_summary":true%' ORDER BY created_at ASC`
: `SELECT id, conversation_id, content, agent_id, model_used, metadata, created_at FROM messages WHERE conversation_id = ? AND agent_id = ? AND role = 'assistant' AND metadata LIKE '%"is_summary":true%' ORDER BY created_at ASC`,
[conversationId, SUMMARY_AGENT_ID]
);
const summaries = summaryMessages.map((m, idx) => {
const meta = m.metadata ? (typeof m.metadata === 'string' ? safeJsonParse(m.metadata, {}) : m.metadata) : {};
return {
id: m.id, conversation_id: m.conversation_id, chunk_number: idx + 1,
messages_start_id: meta.messages_start_id || null, messages_end_id: meta.messages_end_id || null,
messages_count: meta.messages_count || 0, summary: m.content, summary_model: m.model_used || 'unknown',
created_at: m.created_at, agent_id: m.agent_id,
agent_name: meta.agent_name || 'Summary', agent_icon: meta.agent_icon || null, agent_color: meta.agent_color || null,
};
});
return success(res, { summaries });
} catch (err) {
apiLogger.error({ err, conversationId }, 'Error fetching conversation summaries');
return error(res, 'SUMMARIES_LIST_ERROR', err.message, 500);
}
});
// POST /conversations/:id/summaries - Generate a new summary
router.post('/conversations/:id/summaries', requireAuth, async (req, res) => {
const conversationId = Number(req.params.id);
if (!conversationId) return badRequest(res, 'Invalid conversation ID');
try {
const conv = await dbGet(isPostgres() ? 'SELECT id, settings FROM conversations WHERE id = $1' : 'SELECT id, settings FROM conversations WHERE id = ?', [conversationId]);
if (!conv) return notFound(res, 'Conversation');
const messages = await dbAll(
isPostgres()
? `SELECT id, role, content, content_type, sender_type, created_at FROM messages WHERE conversation_id = $1 AND (content_type = 'text' OR content_type IS NULL) ORDER BY created_at ASC`
: `SELECT id, role, content, content_type, sender_type, created_at FROM messages WHERE conversation_id = ? AND (content_type = 'text' OR content_type IS NULL) ORDER BY created_at ASC`,
[conversationId]
);
if (messages.length === 0) return badRequest(res, 'No messages to summarize');
const prompt = generateSummaryPrompt(messages);
const convSettings = safeJsonParse(conv.settings, {});
const summaryModel = convSettings.summary_model || req.body.model || 'gpt-4o-mini';
const summaryAgentId = convSettings.summary_agent_id || req.body.summary_agent_id || null;
let summaryText;
async function tryGenerateSummary(systemPrompt, model) {
let result = await executeSimpleAI({ input: prompt, systemPrompt, model });
if (result?.success && result?.content) return result.content;
apiLogger.warn({ conversationId, error: result?.error, model, success: result?.success }, 'Summary: primary provider failed, trying claude-code fallback');
result = await executeSimpleAI({ input: prompt, systemPrompt, model: 'sonnet', provider: 'claude-code' });
if (result?.success && result?.content) return result.content;
apiLogger.error({ conversationId, error: result?.error, success: result?.success }, 'Summary: all providers failed');
return null;
}
if (summaryAgentId) {
const agentRow = await dbGet(isPostgres() ? `SELECT tr.data FROM table_rows tr WHERE tr.id = $1` : `SELECT tr.data FROM table_rows tr WHERE tr.id = ?`, [summaryAgentId]);
if (agentRow) {
const agentData = safeJsonParse(agentRow.data, {});
const agentConfig = { systemPrompt: agentData.system_prompt || 'You are a concise summarizer. Produce a brief summary of the conversation.', model: agentData.model || summaryModel };
summaryText = await tryGenerateSummary(agentConfig.systemPrompt, agentConfig.model);
} else {
summaryText = await tryGenerateSummary(SUMMARY_SYSTEM_PROMPT, summaryModel);
}
} else {
summaryText = await tryGenerateSummary(SUMMARY_SYSTEM_PROMPT, summaryModel);
}
if (!summaryText) return error(res, 'SUMMARY_GENERATION_FAILED', 'All AI providers failed to generate summary. Check API keys configuration.', 500);
const existingSummaries = await dbAll(
isPostgres()
? `SELECT id FROM messages WHERE conversation_id = $1 AND agent_id = $2 AND role = 'assistant' AND metadata::text LIKE '%"is_summary":true%' ORDER BY created_at ASC`
: `SELECT id FROM messages WHERE conversation_id = ? AND agent_id = ? AND role = 'assistant' AND metadata LIKE '%"is_summary":true%' ORDER BY created_at ASC`,
[conversationId, SUMMARY_AGENT_ID]
);
const nextChunkNumber = existingSummaries.length + 1;
const messagesStartId = messages[0].id;
const messagesEndId = messages[messages.length - 1].id;
let agentName = 'Summary', agentIcon = null, agentColor = '#3b82f6';
try {
const agentRow = await dbGet(isPostgres() ? `SELECT data FROM table_rows WHERE id = $1` : `SELECT data FROM table_rows WHERE id = ?`, [SUMMARY_AGENT_ID]);
if (agentRow) { const agentData = safeJsonParse(agentRow.data, {}); agentName = agentData.name || 'Summary'; agentIcon = agentData.icon || null; agentColor = agentData.color || '#3b82f6'; }
} catch (agentErr) { apiLogger.warn({ err: agentErr.message }, 'Failed to resolve Summary Agent info, using defaults'); }
const summaryMetadata = JSON.stringify({
is_summary: true, agent_name: agentName, agent_icon: agentIcon, agent_color: agentColor, agent_row_id: SUMMARY_AGENT_ID,
chunk_number: nextChunkNumber, messages_start_id: messagesStartId, messages_end_id: messagesEndId, messages_count: messages.length,
});
await saveStepMessage(conversationId, { content: summaryText, contentType: 'text', role: 'assistant', senderType: 'agent', agentId: SUMMARY_AGENT_ID, modelUsed: summaryModel, metadata: summaryMetadata });
await dbRun(
isPostgres()
? `UPDATE conversations SET summary = $1, summary_message_id = (SELECT id FROM messages WHERE conversation_id = $2 AND agent_id = $3 AND role = 'assistant' AND metadata::text LIKE '%"is_summary":true%' ORDER BY created_at DESC LIMIT 1), updated_at = NOW() WHERE id = $2`
: `UPDATE conversations SET summary = ?, summary_message_id = (SELECT id FROM messages WHERE conversation_id = ? AND agent_id = ? AND role = 'assistant' AND metadata LIKE '%"is_summary":true%' ORDER BY created_at DESC LIMIT 1), updated_at = datetime('now') WHERE id = ?`,
isPostgres() ? [summaryText, conversationId, SUMMARY_AGENT_ID] : [summaryText, conversationId, SUMMARY_AGENT_ID, conversationId]
).catch(err => { apiLogger.warn({ err: err.message, conversationId }, 'Failed to update conversations.summary'); });
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, summary_model, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, summary_model, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
[conversationId, nextChunkNumber, messagesStartId, messagesEndId, messages.length, summaryText, summaryModel]
).catch(legacyErr => { apiLogger.warn({ err: legacyErr.message, conversationId }, 'Failed to save to conversation_summaries (legacy), summary message saved successfully'); });
const savedSummary = {
id: null, conversation_id: conversationId, chunk_number: nextChunkNumber,
messages_start_id: messagesStartId, messages_end_id: messagesEndId, messages_count: messages.length,
summary: summaryText, summary_model: summaryModel, created_at: new Date().toISOString(),
agent_id: SUMMARY_AGENT_ID, agent_name: agentName, agent_icon: agentIcon, agent_color: agentColor,
};
apiLogger.info({ conversationId, chunkNumber: nextChunkNumber, agentId: SUMMARY_AGENT_ID }, 'Chat summary saved as agent message');
return created(res, { summary: savedSummary });
} catch (err) {
apiLogger.error({ err, conversationId: req.params.id }, 'Error generating conversation summary');
return error(res, 'SUMMARY_GENERATE_ERROR', err.message, 500);
}
});
}