import { supabaseService } from '../lib/api-client.js';
import { requireAuth } from '../lib/auth.js';
import { logger } from '../lib/logger.js';
import { z } from 'zod';
// Input schemas for AI conversation tools
const SaveConversationSchema = z.object({
project_id: z.string().uuid(),
title: z.string().min(1).max(500).optional(),
messages: z.array(z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string(),
timestamp: z.string().datetime().optional(),
metadata: z.record(z.any()).optional()
})),
context: z.object({
task_id: z.string().uuid().optional(),
document_id: z.string().uuid().optional(),
conversation_type: z.enum(['task_discussion', 'document_review', 'project_planning', 'troubleshooting', 'general']).default('general'),
ai_model: z.string().optional(),
temperature: z.number().min(0).max(2).optional(),
tokens_used: z.number().positive().optional()
}).optional(),
metadata: z.record(z.any()).optional()
});
const GetConversationsSchema = z.object({
project_id: z.string().uuid(),
limit: z.number().int().positive().max(100).default(20),
conversation_type: z.enum(['task_discussion', 'document_review', 'project_planning', 'troubleshooting', 'general']).optional(),
related_to: z.string().uuid().optional(), // task_id or document_id
include_messages: z.boolean().default(true)
});
const AnalyzeConversationSchema = z.object({
conversation_id: z.string().uuid()
});
const ExtractActionItemsSchema = z.object({
conversation_id: z.string().uuid(),
auto_create_tasks: z.boolean().default(false)
});
const GenerateConversationSummarySchema = z.object({
conversation_id: z.string().uuid(),
summary_type: z.enum(['brief', 'detailed', 'action_items', 'decisions']).default('brief')
});
/**
* Save AI conversation with context
*/
export const saveConversationTool = {
name: 'save_conversation',
description: 'Save an AI conversation with project context for future reference and analysis',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
format: 'uuid',
description: 'The project ID this conversation relates to'
},
title: {
type: 'string',
maxLength: 500,
description: 'Optional title for the conversation (auto-generated if not provided)'
},
messages: {
type: 'array',
items: {
type: 'object',
properties: {
role: {
type: 'string',
enum: ['user', 'assistant', 'system'],
description: 'The role of the message sender'
},
content: {
type: 'string',
description: 'The message content'
},
timestamp: {
type: 'string',
format: 'date-time',
description: 'When the message was sent (auto-generated if not provided)'
},
metadata: {
type: 'object',
description: 'Additional metadata for the message'
}
},
required: ['role', 'content']
},
description: 'Array of conversation messages'
},
context: {
type: 'object',
properties: {
task_id: {
type: 'string',
format: 'uuid',
description: 'Related task ID if conversation is about a specific task'
},
document_id: {
type: 'string',
format: 'uuid',
description: 'Related document ID if conversation is about a specific document'
},
conversation_type: {
type: 'string',
enum: ['task_discussion', 'document_review', 'project_planning', 'troubleshooting', 'general'],
description: 'Type of conversation for better categorization'
},
ai_model: {
type: 'string',
description: 'AI model used in the conversation'
},
temperature: {
type: 'number',
minimum: 0,
maximum: 2,
description: 'AI temperature setting used'
},
tokens_used: {
type: 'number',
description: 'Total tokens consumed in the conversation'
}
},
description: 'Context information about the conversation'
},
metadata: {
type: 'object',
description: 'Additional metadata for the conversation'
}
},
required: ['project_id', 'messages']
}
};
export const saveConversation = requireAuth(async (args) => {
const { project_id, title, messages, context, metadata } = SaveConversationSchema.parse(args);
logger.info('Saving AI conversation', {
project_id,
message_count: messages.length,
conversation_type: context?.conversation_type
});
// Process messages and add timestamps if missing
const processedMessages = messages.map(msg => ({
...msg,
timestamp: msg.timestamp || new Date().toISOString(),
metadata: msg.metadata || {}
}));
// Generate title if not provided
const conversationTitle = title || generateConversationTitle(processedMessages, context?.conversation_type);
// Analyze conversation for insights
const analysis = analyzeConversationContent(processedMessages);
// Create conversation record
const conversationData = {
project_id,
title: conversationTitle,
messages: processedMessages,
metadata: {
...metadata,
context: context || {},
analysis,
created_via: 'mcp',
message_count: processedMessages.length,
total_tokens: context?.tokens_used || estimateTokenCount(processedMessages)
}
};
// Save to database (extend the supabase service to handle conversations)
const conversation = await saveConversationToDatabase(conversationData);
// Extract and optionally create action items
const actionItems = extractActionItemsFromConversation(processedMessages, context);
logger.info('Conversation saved successfully', {
conversation_id: conversation.id,
action_items_found: actionItems.length
});
return {
conversation,
analysis,
action_items: actionItems,
insights: generateConversationInsights(analysis, context),
message: `Conversation "${conversationTitle}" saved successfully`
};
});
/**
* Get conversations with filtering
*/
export const getConversationsTool = {
name: 'get_conversations',
description: 'Retrieve AI conversations for a project with optional filtering',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
format: 'uuid',
description: 'The project ID to get conversations for'
},
limit: {
type: 'number',
minimum: 1,
maximum: 100,
default: 20,
description: 'Maximum number of conversations to return'
},
conversation_type: {
type: 'string',
enum: ['task_discussion', 'document_review', 'project_planning', 'troubleshooting', 'general'],
description: 'Filter by conversation type'
},
related_to: {
type: 'string',
format: 'uuid',
description: 'Filter by related task or document ID'
},
include_messages: {
type: 'boolean',
default: true,
description: 'Whether to include full message content'
}
},
required: ['project_id']
}
};
export const getConversations = requireAuth(async (args) => {
const { project_id, limit, conversation_type, related_to, include_messages } = GetConversationsSchema.parse(args);
logger.info('Getting conversations', { project_id, conversation_type, related_to, limit });
// Get conversations from database
const conversations = await getConversationsFromDatabase({
project_id,
limit,
conversation_type,
related_to,
include_messages
});
// Add analytics
const analytics = {
total_conversations: conversations.length,
conversation_types: conversations.reduce((acc, conv) => {
const type = conv.metadata?.context?.conversation_type || 'general';
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {}),
total_messages: conversations.reduce((sum, conv) => sum + (conv.metadata?.message_count || 0), 0),
average_length: conversations.length > 0
? conversations.reduce((sum, conv) => sum + (conv.metadata?.message_count || 0), 0) / conversations.length
: 0,
recent_activity: conversations.filter(conv => new Date(conv.created_at).getTime() > Date.now() - (7 * 24 * 60 * 60 * 1000)).length
};
return {
conversations,
analytics,
filters_applied: { conversation_type, related_to }
};
});
/**
* Analyze conversation for insights
*/
export const analyzeConversationTool = {
name: 'analyze_conversation',
description: 'Analyze an AI conversation to extract insights, themes, and patterns',
inputSchema: {
type: 'object',
properties: {
conversation_id: {
type: 'string',
format: 'uuid',
description: 'The conversation ID to analyze'
}
},
required: ['conversation_id']
}
};
export const analyzeConversation = requireAuth(async (args) => {
const { conversation_id } = AnalyzeConversationSchema.parse(args);
logger.info('Analyzing conversation', { conversation_id });
const conversation = await getConversationFromDatabase(conversation_id);
if (!conversation) {
throw new Error('Conversation not found');
}
const analysis = {
conversation_flow: analyzeConversationFlow(conversation.messages),
content_analysis: analyzeConversationContent(conversation.messages),
ai_performance: analyzeAIPerformance(conversation.messages),
topic_modeling: extractTopicsAndThemes(conversation.messages),
action_items: extractActionItemsFromConversation(conversation.messages, conversation.metadata?.context),
decisions_made: extractDecisions(conversation.messages),
questions_raised: extractQuestions(conversation.messages),
knowledge_gaps: identifyKnowledgeGaps(conversation.messages),
follow_up_suggestions: generateFollowUpSuggestions(conversation.messages, conversation.metadata?.context)
};
return {
conversation_id,
analysis,
summary: generateAnalysisSummary(analysis),
recommendations: generateRecommendations(analysis, conversation.metadata?.context)
};
});
/**
* Extract action items from conversation
*/
export const extractActionItemsTool = {
name: 'extract_action_items',
description: 'Extract actionable items from a conversation and optionally create tasks',
inputSchema: {
type: 'object',
properties: {
conversation_id: {
type: 'string',
format: 'uuid',
description: 'The conversation ID to extract action items from'
},
auto_create_tasks: {
type: 'boolean',
default: false,
description: 'Whether to automatically create tasks for action items'
}
},
required: ['conversation_id']
}
};
export const extractActionItems = requireAuth(async (args) => {
const { conversation_id, auto_create_tasks } = ExtractActionItemsSchema.parse(args);
logger.info('Extracting action items', { conversation_id, auto_create_tasks });
const conversation = await getConversationFromDatabase(conversation_id);
if (!conversation) {
throw new Error('Conversation not found');
}
const actionItems = extractActionItemsFromConversation(conversation.messages, conversation.metadata?.context);
let createdTasks = [];
if (auto_create_tasks && actionItems.length > 0) {
// Create tasks for each action item
for (const item of actionItems) {
try {
const task = await supabaseService.createTask({
project_id: conversation.project_id,
initiative_id: null,
title: item.title,
description: `${item.description}\n\nExtracted from conversation: ${conversation.title}\n\n[Source: AI Conversation ${conversation.id}, Confidence: ${item.confidence}]`,
priority: item.priority || 'medium',
status: 'todo',
due_date: null,
assignee_id: null
});
createdTasks.push(task);
}
catch (error) {
logger.error('Failed to create task from action item', { error, item });
}
}
}
return {
conversation_id,
action_items: actionItems,
created_tasks: createdTasks,
summary: {
total_items: actionItems.length,
high_priority: actionItems.filter(item => item.priority === 'high').length,
tasks_created: createdTasks.length
}
};
});
/**
* Generate conversation summary
*/
export const generateConversationSummaryTool = {
name: 'generate_conversation_summary',
description: 'Generate different types of summaries from a conversation',
inputSchema: {
type: 'object',
properties: {
conversation_id: {
type: 'string',
format: 'uuid',
description: 'The conversation ID to summarize'
},
summary_type: {
type: 'string',
enum: ['brief', 'detailed', 'action_items', 'decisions'],
default: 'brief',
description: 'Type of summary to generate'
}
},
required: ['conversation_id']
}
};
export const generateConversationSummary = requireAuth(async (args) => {
const { conversation_id, summary_type } = GenerateConversationSummarySchema.parse(args);
logger.info('Generating conversation summary', { conversation_id, summary_type });
const conversation = await getConversationFromDatabase(conversation_id);
if (!conversation) {
throw new Error('Conversation not found');
}
let summary;
switch (summary_type) {
case 'brief':
summary = generateBriefSummary(conversation.messages);
break;
case 'detailed':
summary = generateDetailedSummary(conversation.messages, conversation.metadata?.context);
break;
case 'action_items':
summary = generateActionItemsSummary(conversation.messages);
break;
case 'decisions':
summary = generateDecisionsSummary(conversation.messages);
break;
default:
summary = generateBriefSummary(conversation.messages);
}
const metadata = {
conversation_id,
summary_type,
generated_at: new Date().toISOString(),
message_count: conversation.messages.length,
conversation_duration: calculateConversationDuration(conversation.messages),
key_participants: extractParticipants(conversation.messages)
};
return {
summary,
metadata,
word_count: summary.split(' ').length,
reading_time: Math.ceil(summary.split(' ').length / 200) // 200 words per minute
};
});
// Helper functions for conversation analysis
function generateConversationTitle(messages, type) {
if (type) {
const typeLabels = {
task_discussion: 'Task Discussion',
document_review: 'Document Review',
project_planning: 'Project Planning',
troubleshooting: 'Troubleshooting',
general: 'General Discussion'
};
return `${typeLabels[type] || 'Discussion'} - ${new Date().toLocaleDateString()}`;
}
// Extract potential title from first user message
const firstUserMessage = messages.find(m => m.role === 'user');
if (firstUserMessage) {
const firstLine = firstUserMessage.content.split('\n')[0];
if (firstLine.length > 10 && firstLine.length < 100) {
return firstLine.length > 50 ? firstLine.substring(0, 47) + '...' : firstLine;
}
}
return `AI Conversation - ${new Date().toLocaleDateString()}`;
}
function analyzeConversationContent(messages) {
const totalMessages = messages.length;
const userMessages = messages.filter(m => m.role === 'user').length;
const assistantMessages = messages.filter(m => m.role === 'assistant').length;
const totalWords = messages.reduce((sum, msg) => sum + msg.content.split(' ').length, 0);
const avgWordsPerMessage = totalWords / totalMessages;
// Extract common patterns
const questions = messages.filter(msg => msg.content.includes('?')).length;
const codeBlocks = messages.filter(msg => msg.content.includes('```')).length;
const urls = messages.filter(msg => /https?:\/\//.test(msg.content)).length;
return {
message_count: totalMessages,
user_messages: userMessages,
assistant_messages: assistantMessages,
total_words: totalWords,
avg_words_per_message: Math.round(avgWordsPerMessage),
questions_asked: questions,
code_examples: codeBlocks,
external_links: urls,
conversation_balance: userMessages / (assistantMessages || 1)
};
}
function extractActionItemsFromConversation(messages, context) {
const actionItems = [];
// Simple regex patterns for action items
const patterns = [
/(?:need to|should|must|will|todo|action item):?\s*(.+)/gi,
/(?:let's|we'll|i'll|you'll)\s+(.+)/gi,
/(?:next step|follow up|follow-up):?\s*(.+)/gi
];
messages.forEach((message, index) => {
patterns.forEach(pattern => {
const matches = [...message.content.matchAll(pattern)];
matches.forEach(match => {
if (match[1] && match[1].length > 10 && match[1].length < 200) {
actionItems.push({
title: match[1].trim(),
description: `From conversation: "${message.content.substring(0, 100)}..."`,
source_message_index: index,
source_role: message.role,
priority: determinePriority(match[1]),
confidence: calculateConfidence(match[1], message.content),
context
});
}
});
});
});
return actionItems;
}
function determinePriority(text) {
const urgentWords = ['urgent', 'asap', 'immediately', 'critical', 'emergency'];
const highWords = ['important', 'priority', 'must', 'required', 'essential'];
const lowerText = text.toLowerCase();
if (urgentWords.some(word => lowerText.includes(word)))
return 'urgent';
if (highWords.some(word => lowerText.includes(word)))
return 'high';
if (lowerText.includes('should') || lowerText.includes('need'))
return 'medium';
return 'low';
}
function calculateConfidence(text, context) {
let confidence = 50; // Base confidence
// Increase confidence for specific patterns
if (text.includes('will') || text.includes('must'))
confidence += 20;
if (text.includes('should') || text.includes('need to'))
confidence += 15;
if (context.includes('action') || context.includes('todo'))
confidence += 10;
if (text.length > 50)
confidence += 10; // More detailed items
// Decrease confidence for vague language
if (text.includes('maybe') || text.includes('might'))
confidence -= 15;
if (text.includes('probably') || text.includes('perhaps'))
confidence -= 10;
return Math.max(0, Math.min(100, confidence));
}
function estimateTokenCount(messages) {
// Rough estimate: 1 token ≈ 4 characters
const totalChars = messages.reduce((sum, msg) => sum + msg.content.length, 0);
return Math.ceil(totalChars / 4);
}
// Placeholder functions for database operations (to be implemented in supabase service)
async function saveConversationToDatabase(conversationData) {
// This would be implemented in the supabase service
return {
id: 'generated-uuid',
...conversationData,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
}
async function getConversationsFromDatabase(filters) {
// This would be implemented in the supabase service
return [];
}
async function getConversationFromDatabase(id) {
// This would be implemented in the supabase service
return null;
}
// Additional analysis functions
function analyzeConversationFlow(messages) {
return {
turn_taking: messages.reduce((acc, msg, idx) => {
if (idx > 0 && messages[idx - 1].role !== msg.role)
acc++;
return acc;
}, 0),
longest_response: Math.max(...messages.map(m => m.content.length)),
conversation_rhythm: 'balanced' // Would calculate based on message timing
};
}
function analyzeAIPerformance(messages) {
const aiMessages = messages.filter(m => m.role === 'assistant');
return {
response_count: aiMessages.length,
avg_response_length: aiMessages.reduce((sum, msg) => sum + msg.content.length, 0) / (aiMessages.length || 1),
helpful_responses: aiMessages.filter(msg => msg.content.includes('```') ||
msg.content.includes('example') ||
msg.content.length > 100).length
};
}
function extractTopicsAndThemes(messages) {
// Simple keyword extraction
const allText = messages.map(m => m.content).join(' ').toLowerCase();
const commonWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they'];
const words = allText.split(/\s+/).filter(word => word.length > 3 &&
!commonWords.includes(word) &&
/^[a-zA-Z]+$/.test(word));
const wordCount = words.reduce((acc, word) => {
acc[word] = (acc[word] || 0) + 1;
return acc;
}, {});
return Object.entries(wordCount)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([word]) => word);
}
function extractDecisions(messages) {
const decisionPatterns = [
/(?:decided|decision|concluded|agreed|determined):?\s*(.+)/gi,
/(?:we will|we'll|going to)\s+(.+)/gi
];
const decisions = [];
messages.forEach(message => {
decisionPatterns.forEach(pattern => {
const matches = [...message.content.matchAll(pattern)];
matches.forEach(match => {
if (match[1] && match[1].length > 10) {
decisions.push(match[1].trim());
}
});
});
});
return decisions;
}
function extractQuestions(messages) {
return messages
.filter(msg => msg.content.includes('?'))
.map(msg => msg.content.split('?')[0] + '?')
.filter(q => q.length > 10 && q.length < 200);
}
function identifyKnowledgeGaps(messages) {
const gapPatterns = [
/(?:don't know|not sure|unclear|confused|need help):?\s*(.+)/gi,
/(?:how to|what is|what are|where is|when is|why is)\s+(.+)/gi
];
const gaps = [];
messages.forEach(message => {
gapPatterns.forEach(pattern => {
const matches = [...message.content.matchAll(pattern)];
matches.forEach(match => {
if (match[1] && match[1].length > 5) {
gaps.push(match[1].trim());
}
});
});
});
return gaps;
}
function generateFollowUpSuggestions(messages, context) {
const suggestions = [
'Review and prioritize the action items identified',
'Schedule follow-up meeting to track progress',
'Create documentation based on decisions made',
'Share summary with relevant team members'
];
if (context?.task_id) {
suggestions.push('Update the related task with conversation insights');
}
if (context?.document_id) {
suggestions.push('Update the related document with new information');
}
return suggestions;
}
function generateBriefSummary(messages) {
const firstUserMsg = messages.find(m => m.role === 'user');
const lastAssistantMsg = [...messages].reverse().find(m => m.role === 'assistant');
return `Conversation started with: "${firstUserMsg?.content.substring(0, 100)}..." and concluded with AI providing guidance and next steps. ${messages.length} messages exchanged with key topics and action items identified.`;
}
function generateDetailedSummary(messages, context) {
const analysis = analyzeConversationContent(messages);
const actionItems = extractActionItemsFromConversation(messages, context);
const decisions = extractDecisions(messages);
return `
**Conversation Summary**
- **Message Count**: ${analysis.message_count} (${analysis.user_messages} user, ${analysis.assistant_messages} AI)
- **Total Words**: ${analysis.total_words}
- **Questions Asked**: ${analysis.questions_asked}
- **Code Examples**: ${analysis.code_examples}
**Key Outcomes**:
- ${actionItems.length} action items identified
- ${decisions.length} decisions made
- ${analysis.external_links} external resources referenced
**Context**: ${context?.conversation_type || 'General discussion'} related to project activities.
`.trim();
}
function generateActionItemsSummary(messages) {
const actionItems = extractActionItemsFromConversation(messages);
if (actionItems.length === 0) {
return 'No specific action items were identified in this conversation.';
}
return `**Action Items Identified (${actionItems.length})**:\n\n` +
actionItems.map((item, idx) => `${idx + 1}. ${item.title} (${item.priority} priority)`).join('\n');
}
function generateDecisionsSummary(messages) {
const decisions = extractDecisions(messages);
if (decisions.length === 0) {
return 'No explicit decisions were documented in this conversation.';
}
return `**Decisions Made (${decisions.length})**:\n\n` +
decisions.map((decision, idx) => `${idx + 1}. ${decision}`).join('\n');
}
function calculateConversationDuration(messages) {
if (messages.length < 2)
return 0;
const firstTimestamp = messages[0].timestamp;
const lastTimestamp = messages[messages.length - 1].timestamp;
if (!firstTimestamp || !lastTimestamp)
return 0;
const firstTime = new Date(firstTimestamp).getTime();
const lastTime = new Date(lastTimestamp).getTime();
return Math.round((lastTime - firstTime) / (1000 * 60)); // minutes
}
function extractParticipants(messages) {
return [...new Set(messages.map(m => m.role))];
}
function generateAnalysisSummary(analysis) {
return `Conversation analysis complete: ${analysis.content_analysis.message_count} messages analyzed, ${analysis.action_items.length} action items found, ${analysis.decisions_made.length} decisions identified.`;
}
function generateRecommendations(analysis, context) {
const recommendations = [];
if (analysis.action_items.length > 0) {
recommendations.push('Convert action items into trackable tasks');
}
if (analysis.knowledge_gaps.length > 0) {
recommendations.push('Address identified knowledge gaps through documentation');
}
if (analysis.questions_raised.length > analysis.action_items.length) {
recommendations.push('Many questions were raised - consider scheduling follow-up session');
}
return recommendations;
}
function generateConversationInsights(analysis, context) {
return {
productivity_score: Math.round((analysis.total_words / analysis.message_count) * 0.1),
engagement_level: analysis.conversation_balance > 0.8 ? 'high' : 'moderate',
actionability: analysis.questions > 0 ? 'high' : 'low',
follow_up_needed: analysis.questions > 2 || analysis.code_examples > 0
};
}
// Export all conversation tools
export const conversationTools = {
saveConversationTool,
getConversationsTool,
analyzeConversationTool,
extractActionItemsTool,
generateConversationSummaryTool
};
export const conversationHandlers = {
save_conversation: saveConversation,
get_conversations: getConversations,
analyze_conversation: analyzeConversation,
extract_action_items: extractActionItems,
generate_conversation_summary: generateConversationSummary
};