devcontext
Version:
DevContext is a cutting-edge Model Context Protocol (MCP) server designed to provide developers with continuous, project-centric context awareness.
1,294 lines (1,163 loc) • 40.1 kB
JavaScript
/**
* finalizeConversationContext.tool.js
*
* MCP tool implementation for finalizing a conversation context
* This tool performs learning extraction, pattern promotion, and generates insights
* when a conversation ends.
*/
import { z } from "zod";
import { executeQuery } from "../db.js";
import * as ConversationIntelligence from "../logic/ConversationIntelligence.js";
import * as TimelineManagerLogic from "../logic/TimelineManagerLogic.js";
import * as ActiveContextManager from "../logic/ActiveContextManager.js";
import * as LearningSystems from "../logic/LearningSystems.js";
import * as GlobalPatternRepository from "../logic/GlobalPatternRepository.js";
import * as SmartSearchServiceLogic from "../logic/SmartSearchServiceLogic.js";
import * as ContextCompressorLogic from "../logic/ContextCompressorLogic.js";
import * as TextTokenizerLogic from "../logic/TextTokenizerLogic.js";
import { logMessage } from "../utils/logger.js";
import { v4 as uuidv4 } from "uuid";
import {
finalizeConversationContextInputSchema,
finalizeConversationContextOutputSchema,
} from "../schemas/toolSchemas.js";
/**
* Handler for finalize_conversation_context tool
*
* @param {object} input - Tool input parameters
* @param {object} sdkContext - SDK context
* @returns {Promise<object>} Tool output
*/
async function handler(input, sdkContext) {
try {
logMessage("INFO", `finalize_conversation_context tool started`, {
conversationId: input.conversationId,
outcome: input.outcome || "completed",
clearActiveContext: input.clearActiveContext || false,
});
// 1. Extract input parameters
const {
conversationId,
clearActiveContext = false,
extractLearnings = true,
promotePatterns = true,
synthesizeRelatedTopics = true,
generateNextSteps = true,
outcome = "completed",
} = input;
// Validate conversation ID
if (!conversationId) {
const error = new Error("Conversation ID is required");
error.code = "MISSING_CONVERSATION_ID";
throw error;
}
logMessage("DEBUG", `Processing options`, {
extractLearnings,
promotePatterns,
synthesizeRelatedTopics,
generateNextSteps,
});
// 2. Fetch conversation history, purpose, and topics
let conversationHistory = [];
let conversationPurpose = null;
let conversationTopics = [];
try {
conversationHistory =
await ConversationIntelligence.getConversationHistory(conversationId);
if (!conversationHistory || conversationHistory.length === 0) {
const error = new Error(
`No conversation history found for ID: ${conversationId}`
);
error.code = "CONVERSATION_NOT_FOUND";
throw error;
}
logMessage("DEBUG", `Retrieved conversation history`, {
messageCount: conversationHistory.length,
});
} catch (historyErr) {
logMessage("ERROR", `Failed to retrieve conversation history`, {
error: historyErr.message,
conversationId,
});
throw historyErr; // This is critical, rethrow
}
// Get conversation purpose
try {
conversationPurpose =
await ConversationIntelligence.getConversationPurpose(conversationId);
logMessage(
"DEBUG",
`Retrieved conversation purpose: ${conversationPurpose || "Unknown"}`
);
} catch (purposeErr) {
logMessage("WARN", `Failed to retrieve conversation purpose`, {
error: purposeErr.message,
conversationId,
});
// Continue without purpose
}
// Get conversation topics
try {
conversationTopics = await ConversationIntelligence.getConversationTopics(
conversationId
);
logMessage(
"DEBUG",
`Retrieved ${conversationTopics.length} conversation topics`
);
} catch (topicsErr) {
logMessage("WARN", `Failed to retrieve conversation topics`, {
error: topicsErr.message,
conversationId,
});
// Continue with empty topics
conversationTopics = [];
}
// 3. Generate overall conversation summary
let summary = "";
try {
summary = await ConversationIntelligence.summarizeConversation(
conversationId
);
logMessage("INFO", `Generated conversation summary`, {
summaryLength: summary.length,
});
} catch (summaryErr) {
logMessage("WARN", `Failed to generate conversation summary`, {
error: summaryErr.message,
conversationId,
});
// Use a basic summary as fallback
summary = `Conversation ${conversationId} with ${conversationHistory.length} messages`;
}
// 4. Record conversation_end event in the timeline
try {
await TimelineManagerLogic.recordEvent(
"conversation_end",
{
summary,
purpose: conversationPurpose,
topics: conversationTopics.length,
outcome,
},
[], // No specific entities for conversation end
conversationId
);
logMessage("DEBUG", `Recorded conversation_end event in timeline`);
} catch (timelineErr) {
logMessage("WARN", `Failed to record conversation_end event`, {
error: timelineErr.message,
conversationId,
});
// Continue despite timeline error
}
// 5. Initialize result objects
let extractedLearnings = null;
let promotedPatterns = null;
let relatedConversations = null;
let nextSteps = null;
// 6. Extract learnings if requested
if (extractLearnings) {
try {
logMessage("INFO", `Extracting learnings from conversation`);
extractedLearnings = await _extractConversationLearnings(
conversationId,
conversationHistory
);
logMessage(
"INFO",
`Extracted ${
extractedLearnings?.patterns?.length || 0
} patterns and ${
extractedLearnings?.bugPatterns?.length || 0
} bug patterns`
);
} catch (learningErr) {
logMessage("WARN", `Failed to extract learnings`, {
error: learningErr.message,
conversationId,
});
// Continue without learnings
extractedLearnings = {
patterns: [],
bugPatterns: [],
conceptualInsights: [],
error: learningErr.message,
};
}
} else {
logMessage("DEBUG", `Skipping learning extraction (not requested)`);
}
// 7. Promote patterns if requested
if (promotePatterns) {
try {
logMessage("INFO", `Promoting patterns from conversation`);
promotedPatterns = await _promoteConversationPatterns(
conversationId,
outcome
);
logMessage("INFO", `Promoted ${promotedPatterns?.count || 0} patterns`);
} catch (patternErr) {
logMessage("WARN", `Failed to promote patterns`, {
error: patternErr.message,
conversationId,
});
// Continue without pattern promotion
promotedPatterns = {
count: 0,
patterns: [],
error: patternErr.message,
};
}
} else {
logMessage("DEBUG", `Skipping pattern promotion (not requested)`);
}
// 8. Synthesize related topics if requested
if (synthesizeRelatedTopics) {
try {
logMessage("INFO", `Finding and synthesizing related conversations`);
relatedConversations = await _findAndSynthesizeRelatedConversations(
conversationId,
conversationTopics,
conversationPurpose
);
logMessage(
"INFO",
`Found ${
relatedConversations?.conversations?.length || 0
} related conversations`
);
} catch (relatedErr) {
logMessage("WARN", `Failed to synthesize related conversations`, {
error: relatedErr.message,
conversationId,
});
// Continue without related conversations
relatedConversations = {
conversations: [],
insights: [],
error: relatedErr.message,
};
}
} else {
logMessage("DEBUG", `Skipping related topic synthesis (not requested)`);
}
// 9. Generate next step suggestions if requested
if (generateNextSteps) {
try {
logMessage("INFO", `Generating next step suggestions`);
nextSteps = await _generateNextStepSuggestions(
conversationId,
conversationPurpose,
summary,
extractedLearnings
);
logMessage(
"INFO",
`Generated ${
nextSteps?.suggestions?.length || 0
} next step suggestions`
);
} catch (nextStepsErr) {
logMessage("WARN", `Failed to generate next step suggestions`, {
error: nextStepsErr.message,
conversationId,
});
// Continue without next steps
nextSteps = {
suggestions: [],
error: nextStepsErr.message,
};
}
} else {
logMessage("DEBUG", `Skipping next step generation (not requested)`);
}
// 10. Clear active context if requested
if (clearActiveContext) {
try {
await ActiveContextManager.clearActiveContext();
logMessage("INFO", `Cleared active context`);
} catch (clearErr) {
logMessage("WARN", `Failed to clear active context`, {
error: clearErr.message,
});
// Continue despite error
}
}
// Mark all active conversation purposes as ended
try {
const currentTime = new Date().toISOString();
const updatePurposeQuery = `
UPDATE conversation_purposes
SET end_timestamp = ?
WHERE conversation_id = ? AND end_timestamp IS NULL
`;
await executeQuery(updatePurposeQuery, [currentTime, conversationId]);
logMessage("INFO", `Marked all active conversation purposes as ended`);
// Check if we have any purpose records at all for this conversation
const checkPurposeQuery = `
SELECT COUNT(*) as count FROM conversation_purposes
WHERE conversation_id = ?
`;
const purposeCount = await executeQuery(checkPurposeQuery, [
conversationId,
]);
const hasAnyPurpose = purposeCount?.rows?.[0]?.count > 0;
// If no purpose exists, create a general_query purpose that's already ended
if (!hasAnyPurpose) {
const purposeId = uuidv4();
const startTime = new Date(Date.now() - 60000).toISOString(); // 1 minute ago
const insertPurposeQuery = `
INSERT INTO conversation_purposes (
purpose_id, conversation_id, purpose_type, confidence,
start_timestamp, end_timestamp, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?)
`;
await executeQuery(insertPurposeQuery, [
purposeId,
conversationId,
"general_query",
0.8,
startTime,
currentTime,
JSON.stringify({ source: "finalization", outcome }),
]);
logMessage(
"INFO",
`Created general_query purpose record for finalization`
);
}
} catch (purposeErr) {
logMessage("WARN", `Failed to finalize conversation purposes`, {
error: purposeErr.message,
conversationId,
});
// Continue despite purpose update error
}
// 11. Return the finalized conversation data
logMessage(
"INFO",
`finalize_conversation_context tool completed successfully`
);
const responseData = {
message: `Conversation ${conversationId} finalized successfully with outcome: ${outcome}`,
status: "success",
summary,
purpose: conversationPurpose || "Unknown purpose",
extractedLearnings,
promotedPatterns,
relatedConversations,
nextSteps,
};
return {
content: [
{
type: "text",
text: JSON.stringify(responseData),
},
],
};
} catch (error) {
// Log detailed error information
logMessage("ERROR", `Error in finalize_conversation_context tool`, {
error: error.message,
stack: error.stack,
input: {
conversationId: input.conversationId,
outcome: input.outcome,
},
});
// Return error response
const errorResponse = {
error: true,
errorCode: error.code || "FINALIZATION_FAILED",
errorDetails: error.message,
summary: "Failed to finalize conversation context",
purpose: "Unknown due to error",
extractedLearnings: null,
promotedPatterns: null,
relatedConversations: null,
nextSteps: null,
};
return {
content: [
{
type: "text",
text: JSON.stringify(errorResponse),
},
],
};
}
}
/**
* Extracts learnings from a conversation history - patterns, bugs, etc.
*
* @param {string} conversationId - Conversation ID
* @param {boolean} extractPatterns - Whether to extract code patterns
* @param {boolean} promotePatterns - Whether to promote high-quality patterns to global
* @returns {Promise<{patternCount: number, bugPatternCount: number}>} Counts of extracted items
*/
async function _extractConversationLearnings(
conversationId,
extractPatterns = true,
promotePatterns = false
) {
try {
logMessage("INFO", "Extracting learnings from conversation");
let patternCount = 0;
let bugPatternCount = 0;
// 1. Extract code patterns if requested
if (extractPatterns) {
try {
const extractedPatterns =
await LearningSystems.extractPatternsFromConversation(conversationId);
if (extractedPatterns && extractedPatterns.length > 0) {
patternCount = extractedPatterns.length;
// Store patterns
// TODO: Add storage logic here
// Promote patterns if requested
if (promotePatterns && extractedPatterns.length > 0) {
// TODO: Add promotion logic here
}
}
} catch (error) {
logMessage("WARN", `Failed to extract patterns: ${error.message}`);
}
}
// 2. Extract bug patterns (always do this)
try {
const bugPatterns =
await LearningSystems.extractBugPatternsFromConversation(
conversationId
);
if (bugPatterns && bugPatterns.length > 0) {
bugPatternCount = bugPatterns.length;
// Store bug patterns
// TODO: Add storage logic here
}
} catch (error) {
logMessage("WARN", `Failed to extract bug patterns: ${error.message}`);
}
// 3. Extract key-value pairs for potential knowledge base entries
try {
const keyValuePairs = await LearningSystems.extractKeyValuePairs(
conversationId
);
if (keyValuePairs && keyValuePairs.length > 0) {
// Store key-value pairs
// TODO: Add storage logic here
}
} catch (error) {
logMessage("WARN", `Failed to extract key-value pairs: ${error.message}`);
}
logMessage(
"INFO",
`Extracted ${patternCount} patterns and ${bugPatternCount} bug patterns`
);
return {
patternCount,
bugPatternCount,
};
} catch (error) {
logMessage(
"ERROR",
`Error extracting conversation learnings: ${error.message}`
);
return {
patternCount: 0,
bugPatternCount: 0,
};
}
}
/**
* Extract conceptual insights from message content
*
* @param {Array} userMessages - User messages from the conversation
* @param {Array} assistantMessages - Assistant messages from the conversation
* @returns {Promise<Array>} Extracted conceptual insights
* @private
*/
async function _extractConcepts(userMessages, assistantMessages) {
try {
logMessage(
"DEBUG",
`Extracting concepts from ${userMessages.length} user messages and ${assistantMessages.length} assistant messages`
);
// Combine message content for processing
const userContent = userMessages.map((msg) => msg.content).join("\n");
const assistantContent = assistantMessages
.map((msg) => msg.content)
.join("\n");
// Tokenize content to extract key terms
const userTokens = TextTokenizerLogic.tokenize(userContent);
const assistantTokens = TextTokenizerLogic.tokenize(assistantContent);
// Get top terms by frequency
const userTerms = _getTopTermsByFrequency(userTokens, 20);
const assistantTerms = _getTopTermsByFrequency(assistantTokens, 20);
// Find common terms that appear in both user and assistant messages
const commonTerms = userTerms.filter((term) =>
assistantTerms.some((aterm) => aterm.term === term.term)
);
// Extract domain-specific insights
const domainInsights = commonTerms.map((term) => {
// Find relevant snippets containing this term
const snippets = _findRelevantSnippets(
[...userMessages, ...assistantMessages],
term.term
);
return {
concept: term.term,
frequency: term.frequency,
importance: term.frequency / userTokens.length, // Simple importance heuristic
relatedTerms: assistantTerms
.filter(
(aterm) =>
_areTermsRelated(term.term, aterm.term) &&
aterm.term !== term.term
)
.map((aterm) => aterm.term)
.slice(0, 5),
snippets: snippets.slice(0, 3), // Limit to 3 snippets
};
});
logMessage("DEBUG", `Extracted ${domainInsights.length} domain insights`);
return domainInsights;
} catch (error) {
logMessage("ERROR", `Error extracting concepts`, {
error: error.message,
});
throw error;
}
}
/**
* Gets the top terms by frequency from a list of tokens
*
* @param {Array} tokens - Array of tokens
* @param {number} limit - Maximum number of terms to return
* @returns {Array} Array of objects containing term and frequency
* @private
*/
function _getTopTermsByFrequency(tokens, limit = 20) {
try {
if (!tokens || !Array.isArray(tokens) || tokens.length === 0) {
return [];
}
// Count term frequencies
const termCounts = {};
for (const token of tokens) {
if (token && typeof token === "string" && token.length > 2) {
// Skip very short tokens
const term = token.toLowerCase();
termCounts[term] = (termCounts[term] || 0) + 1;
}
}
// Convert to array and sort by frequency
const sortedTerms = Object.entries(termCounts)
.map(([term, frequency]) => ({ term, frequency }))
.sort((a, b) => b.frequency - a.frequency);
// Return top terms
return sortedTerms.slice(0, limit);
} catch (error) {
logMessage("ERROR", `Error getting top terms by frequency`, {
error: error.message,
});
return [];
}
}
/**
* Checks if two terms are related
*
* @param {string} term1 - First term
* @param {string} term2 - Second term
* @returns {boolean} True if the terms are related
* @private
*/
function _areTermsRelated(term1, term2) {
// Simple relation check - one term contains the other
return term1.includes(term2) || term2.includes(term1);
}
/**
* Finds snippets in messages that contain a specific term
*
* @param {Array} messages - Messages to search
* @param {string} term - Term to search for
* @returns {Array} Relevant snippets
* @private
*/
function _findRelevantSnippets(messages, term) {
try {
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return [];
}
const snippets = [];
const termRegex = new RegExp(`\\b${term}\\b`, "i");
for (const message of messages) {
if (!message.content) continue;
// If term found in message
if (termRegex.test(message.content)) {
// Extract a snippet around the term
const sentences = message.content.split(/[.!?]+/);
for (const sentence of sentences) {
if (termRegex.test(sentence)) {
snippets.push({
text: sentence.trim(),
role: message.role,
messageId: message.message_id || message.messageId,
});
}
}
}
}
return snippets;
} catch (error) {
logMessage("WARN", `Error finding relevant snippets`, {
error: error.message,
});
return [];
}
}
/**
* Promotes patterns from a conversation to the global pattern repository
*
* @param {string} conversationId - The ID of the conversation
* @param {string} outcome - The outcome of the conversation
* @returns {Promise<Object>} Promoted patterns data
* @private
*/
async function _promoteConversationPatterns(conversationId, outcome) {
try {
console.log(
`[_promoteConversationPatterns] Promoting patterns for conversation ${conversationId}`
);
// 1. Extract patterns from the conversation
const patterns = await LearningSystems.extractPatternsFromConversation(
conversationId
);
if (!patterns || patterns.length === 0) {
console.log(
`[_promoteConversationPatterns] No patterns found in conversation ${conversationId}`
);
return {
promoted: 0,
patterns: [],
};
}
console.log(
`[_promoteConversationPatterns] Found ${patterns.length} patterns to evaluate for promotion`
);
// 2. Prepare data for tracking promotion results
const promotedPatterns = {
promoted: 0,
patterns: [],
};
// Set minimum confidence threshold based on outcome
let minConfidence = 0.5; // Default threshold
if (outcome === "completed") minConfidence = 0.6;
if (outcome === "abandoned") minConfidence = 0.7; // Higher threshold for abandoned conversations
// 3. Process each pattern for potential promotion
for (const pattern of patterns) {
try {
// Skip patterns that are already global
if (pattern.is_global) {
promotedPatterns.patterns.push({
patternId: pattern.pattern_id,
name: pattern.name,
type: pattern.pattern_type,
promoted: false,
confidence: pattern.confidence_score,
});
continue;
}
// Skip patterns with confidence below threshold
if (pattern.confidence_score < minConfidence) {
promotedPatterns.patterns.push({
patternId: pattern.pattern_id,
name: pattern.name,
type: pattern.pattern_type,
promoted: false,
confidence: pattern.confidence_score,
});
continue;
}
// Promote pattern to global repository
await GlobalPatternRepository.promotePatternToGlobal(
pattern.pattern_id,
pattern.confidence_score
);
// Reinforce the pattern based on conversation outcome
const observationType =
outcome === "completed" || outcome === "reference_only"
? "confirmation"
: "usage";
await GlobalPatternRepository.reinforcePattern(
pattern.pattern_id,
observationType,
{ conversationId }
);
// Record successful promotion
promotedPatterns.promoted++;
promotedPatterns.patterns.push({
patternId: pattern.pattern_id,
name: pattern.name,
type: pattern.pattern_type,
promoted: true,
confidence: pattern.confidence_score,
});
console.log(
`[_promoteConversationPatterns] Successfully promoted pattern ${pattern.pattern_id}`
);
} catch (error) {
console.warn(
`[_promoteConversationPatterns] Error processing pattern ${pattern.pattern_id}:`,
error
);
// Continue with next pattern
}
}
console.log(
`[_promoteConversationPatterns] Promoted ${promotedPatterns.promoted} patterns to global repository`
);
return promotedPatterns;
} catch (error) {
console.error(
`[_promoteConversationPatterns] Error promoting patterns:`,
error
);
return {
promoted: 0,
patterns: [],
error: error.message,
};
}
}
/**
* Finds and synthesizes insights from related conversations
*
* @param {string} conversationId - The ID of the current conversation
* @param {Array} conversationTopics - Topics from the current conversation
* @param {string} conversationPurpose - Purpose of the current conversation
* @returns {Promise<Object>} Related conversations data with synthesized insights
* @private
*/
async function _findAndSynthesizeRelatedConversations(
conversationId,
conversationTopics,
conversationPurpose
) {
try {
console.log(
`[_findAndSynthesizeRelatedConversations] Finding related conversations for ${conversationId}`
);
// 1. Extract keywords from conversation topics
const topicKeywords = new Set();
// Ensure conversationTopics is an array before using forEach
if (conversationTopics && Array.isArray(conversationTopics)) {
conversationTopics.forEach((topic) => {
if (topic.keywords && Array.isArray(topic.keywords)) {
topic.keywords.forEach((kw) => topicKeywords.add(kw));
}
});
} else {
console.warn(
`[_findAndSynthesizeRelatedConversations] conversationTopics is not an array:`,
typeof conversationTopics
);
}
const keywordArray = Array.from(topicKeywords);
// 2. Get recent conversation events from timeline (excluding current conversation)
const recentConversationEvents = await TimelineManagerLogic.getEvents({
types: ["conversation_end", "conversation_completed"],
limit: 10,
excludeConversationId: conversationId,
});
if (!recentConversationEvents || recentConversationEvents.length === 0) {
console.log(
`[_findAndSynthesizeRelatedConversations] No recent conversations found to compare`
);
return {
relatedCount: 0,
conversations: [],
synthesizedInsights: [],
};
}
// 3. Score conversations by relevance
const scoredConversations = [];
for (const event of recentConversationEvents) {
try {
if (!event.data || !event.conversation_id) continue;
// Get conversation topics for comparison
const eventTopics =
await ConversationIntelligence.getConversationTopics(
event.conversation_id
);
// Extract keywords from event topics
const eventKeywords = new Set();
// Ensure eventTopics is an array before using forEach
if (eventTopics && Array.isArray(eventTopics)) {
eventTopics.forEach((topic) => {
if (topic.keywords && Array.isArray(topic.keywords)) {
topic.keywords.forEach((kw) => eventKeywords.add(kw));
}
});
} else {
console.warn(
`[_findAndSynthesizeRelatedConversations] eventTopics for ${event.conversation_id} is not an array`
);
continue; // Skip this event if topics aren't available
}
// Calculate keyword overlap (Jaccard similarity)
const overlapCount = keywordArray.filter((kw) =>
eventKeywords.has(kw)
).length;
const totalUniqueKeywords = new Set([...keywordArray, ...eventKeywords])
.size;
const similarityScore =
totalUniqueKeywords > 0 ? overlapCount / totalUniqueKeywords : 0;
// Find common topics by name
const commonTopics = [];
// Make sure both are arrays before finding common topics
if (
eventTopics &&
Array.isArray(eventTopics) &&
conversationTopics &&
Array.isArray(conversationTopics)
) {
eventTopics.forEach((eventTopic) => {
conversationTopics.forEach((currentTopic) => {
if (
eventTopic.topic_name &&
currentTopic.topic_name &&
eventTopic.topic_name.toLowerCase() ===
currentTopic.topic_name.toLowerCase()
) {
commonTopics.push(eventTopic.topic_name);
}
});
});
}
// Only consider conversations with some similarity
if (similarityScore > 0.2 || commonTopics.length > 0) {
scoredConversations.push({
conversationId: event.conversation_id,
summary: event.data.summary || "No summary available",
timestamp: event.timestamp,
similarityScore,
commonTopics,
});
}
} catch (error) {
console.warn(
`[_findAndSynthesizeRelatedConversations] Error processing event ${event.event_id}:`,
error
);
// Continue with next event
}
}
// Sort by similarity score descending
scoredConversations.sort((a, b) => b.similarityScore - a.similarityScore);
// Limit to top 5 most similar
const relatedConversations = scoredConversations.slice(0, 5);
console.log(
`[_findAndSynthesizeRelatedConversations] Found ${relatedConversations.length} related conversations`
);
// 4. Synthesize insights from related conversations
const synthesizedInsights =
await _synthesizeInsightsFromRelatedConversations(
relatedConversations,
conversationPurpose
);
return {
relatedCount: relatedConversations.length,
conversations: relatedConversations,
synthesizedInsights,
};
} catch (error) {
console.error(
`[_findAndSynthesizeRelatedConversations] Error finding related conversations:`,
error
);
return {
relatedCount: 0,
conversations: [],
synthesizedInsights: [],
error: error.message,
};
}
}
/**
* Synthesizes insights from related conversations
*
* @param {Array} relatedConversations - Array of related conversation data
* @param {string} currentPurpose - Purpose of the current conversation
* @returns {Promise<Array>} Array of synthesized insights by topic
* @private
*/
async function _synthesizeInsightsFromRelatedConversations(
relatedConversations,
currentPurpose
) {
try {
// If no related conversations, return empty insights
if (!relatedConversations || relatedConversations.length === 0) {
return [];
}
// Group conversations by common topics
const conversationsByTopic = {};
// First, identify common topics across conversations
relatedConversations.forEach((conversation) => {
if (conversation.commonTopics && conversation.commonTopics.length > 0) {
conversation.commonTopics.forEach((topic) => {
if (!conversationsByTopic[topic]) {
conversationsByTopic[topic] = [];
}
conversationsByTopic[topic].push(conversation);
});
}
});
// If there are no common topics, create a synthetic topic based on purpose
if (Object.keys(conversationsByTopic).length === 0 && currentPurpose) {
const syntheticTopic = `Conversations about ${currentPurpose}`;
conversationsByTopic[syntheticTopic] = relatedConversations;
}
// Generate insights for each topic group
const insights = [];
for (const [topic, conversations] of Object.entries(conversationsByTopic)) {
// Only synthesize if we have enough conversations on this topic
if (conversations.length >= 2) {
// Combine summaries for synthesis
const combinedSummaries = conversations
.map((c) => c.summary)
.join(" | ");
// Generate synthesized insight using ContextCompressorLogic
const insight = await ContextCompressorLogic.summarizeText(
combinedSummaries,
{
targetLength: 150,
preserveKeyPoints: true,
}
);
insights.push({
topic,
insight,
conversationCount: conversations.length,
sourceSummaries: conversations.map((c) => ({
conversationId: c.conversationId,
summary: c.summary,
})),
});
}
}
return insights;
} catch (error) {
console.error(
`[_synthesizeInsightsFromRelatedConversations] Error synthesizing insights:`,
error
);
return [];
}
}
/**
* Generates next step suggestions based on conversation analysis
*
* @param {string} conversationId - The ID of the conversation
* @param {string|Object} purpose - The purpose of the conversation (string or object with purposeType)
* @param {string} summary - The conversation summary
* @param {Object} extractedLearnings - The extracted learnings from the conversation
* @returns {Promise<Object>} Next steps recommendations
* @private
*/
async function _generateNextStepSuggestions(
conversationId,
purpose,
summary,
extractedLearnings
) {
try {
console.log(
`[_generateNextStepSuggestions] Generating next steps for conversation ${conversationId}`
);
// Initialize results
const result = {
suggestedNextSteps: [],
followUpTopics: [],
referenceMaterials: [],
};
// 1. Extract key terms from summary for searching reference materials
const tokens = TextTokenizerLogic.tokenize(summary);
const keywords = TextTokenizerLogic.extractKeywords(tokens, 10);
// Extract purposeType from purpose object or use purpose directly if it's a string
let purposeType = "general_query"; // Default purpose type
if (purpose) {
if (typeof purpose === "string") {
purposeType = purpose;
} else if (typeof purpose === "object" && purpose.purposeType) {
purposeType = purpose.purposeType;
} else if (typeof purpose === "object" && purpose.purpose_type) {
purposeType = purpose.purpose_type;
}
}
console.log(
`[_generateNextStepSuggestions] Using purpose type: ${purposeType}`
);
// Use purpose to determine likely next steps
let nextSteps = [];
let followUpTopics = [];
if (purposeType) {
// Convert purposeType to lowercase string for safer comparison
const purposeTypeLower =
typeof purposeType === "string"
? purposeType.toLowerCase()
: "general_query";
// Different next steps based on conversation purpose
switch (purposeTypeLower) {
case "debugging":
case "bug_fixing":
nextSteps.push({
action: "Create a test case that verifies the bug fix",
priority: "high",
rationale: "Ensure the bug doesn't reoccur in the future",
});
nextSteps.push({
action: "Document the root cause and solution",
priority: "medium",
rationale: "Help prevent similar issues in the future",
});
break;
case "feature_planning":
case "design_discussion":
nextSteps.push({
action: "Create tickets/tasks for implementation work",
priority: "high",
rationale: "Break down the feature into manageable pieces",
});
nextSteps.push({
action: "Draft initial implementation plan with milestones",
priority: "medium",
rationale: "Establish a timeline and checkpoints",
});
break;
case "code_review":
nextSteps.push({
action: "Address feedback points and resubmit for review",
priority: "high",
rationale: "Incorporate the suggested improvements",
});
nextSteps.push({
action: "Update documentation to reflect changes",
priority: "medium",
rationale: "Keep documentation in sync with code",
});
break;
case "onboarding":
case "knowledge_sharing":
nextSteps.push({
action: "Create summary documentation of discussed topics",
priority: "high",
rationale: "Solidify knowledge transfer",
});
nextSteps.push({
action: "Schedule follow-up session for additional questions",
priority: "medium",
rationale: "Address remaining questions after initial processing",
});
break;
default:
// Generic next steps
nextSteps.push({
action: "Document key decisions from the conversation",
priority: "medium",
rationale: "Preserve important context for future reference",
});
}
}
// 2. Add follow-up topics based on extracted learnings
if (extractedLearnings && extractedLearnings.learnings) {
// Find design decisions that may need follow-up
const designDecisions = extractedLearnings.learnings.filter(
(l) => l.type === "design_decision"
);
if (designDecisions.length > 0) {
const highConfidenceDecisions = designDecisions
.filter((d) => d.confidence >= 0.7)
.slice(0, 2);
highConfidenceDecisions.forEach((decision) => {
followUpTopics.push({
topic: `Implementation details for: ${decision.content}`,
priority: "high",
rationale: "Turn design decision into concrete implementation",
});
});
}
// Find bug patterns that may need follow-up
const bugPatterns = extractedLearnings.learnings.filter(
(l) => l.type === "bug_pattern"
);
if (bugPatterns.length > 0) {
const criticalBugs = bugPatterns
.filter((b) => b.confidence >= 0.8)
.slice(0, 2);
criticalBugs.forEach((bug) => {
followUpTopics.push({
topic: `Root cause analysis for: ${bug.content}`,
priority: "medium",
rationale: "Prevent similar bugs in the future",
});
});
}
}
// 3. Search for reference materials based on keywords
try {
// Only proceed with search if keywords are valid strings
const validKeywords = Array.isArray(keywords)
? keywords.filter((kw) => typeof kw === "string")
: [];
if (validKeywords.length > 0) {
const referenceResults = await SmartSearchServiceLogic.searchByKeywords(
validKeywords,
{
fileTypes: ["md", "txt", "rst", "pdf", "doc"],
maxResults: 5,
searchDocumentation: true,
}
);
const referenceMaterials = referenceResults.map((result) => ({
title: result.name || result.file_path || "Unnamed reference",
path: result.file_path,
type: result.entity_type || "document",
relevance: result.score || 0.5,
}));
result.referenceMaterials = referenceMaterials;
}
} catch (error) {
console.error(`Error in searchByKeywords:`, error);
// Continue with empty reference materials
}
// 4. Combine all results
result.suggestedNextSteps = nextSteps;
result.followUpTopics = followUpTopics;
console.log(
`[_generateNextStepSuggestions] Generated ${nextSteps.length} next steps and ${followUpTopics.length} follow-up topics`
);
return result;
} catch (error) {
console.error(
`[_generateNextStepSuggestions] Error generating next steps:`,
error
);
return {
suggestedNextSteps: [],
followUpTopics: [],
referenceMaterials: [],
error: error.message,
};
}
}
// Export the tool definition for server registration
export default {
name: "finalize_conversation_context",
description:
"Finalizes a conversation context, extracting learnings, promoting patterns, and generating insights",
inputSchema: finalizeConversationContextInputSchema,
outputSchema: finalizeConversationContextOutputSchema,
handler,
};