UNPKG

tryaii-mcp-server

Version:

TryAII MCP Server - 15+ AI models with comparison, cost tracking, and collective intelligence

414 lines 19.2 kB
import { logger } from '../utils/logger.js'; import { databaseService } from './database.js'; import { createChatSessionModel } from '../models/sessions/ChatSession.js'; import { createCompareSessionModel } from '../models/sessions/CompareSession.js'; import { createBrainsSessionModel } from '../models/sessions/BrainsSession.js'; import { createModelsSessionModel } from '../models/sessions/ModelsSession.js'; export class SessionService { static instance; ChatSessionModel; CompareSessionModel; BrainsSessionModel; ModelsSessionModel; constructor() { } static getInstance() { if (!SessionService.instance) { SessionService.instance = new SessionService(); } return SessionService.instance; } async initialize() { try { const mcpConnection = databaseService.getMcpConnection(); this.ChatSessionModel = createChatSessionModel(mcpConnection); this.CompareSessionModel = createCompareSessionModel(mcpConnection); this.BrainsSessionModel = createBrainsSessionModel(mcpConnection); this.ModelsSessionModel = createModelsSessionModel(mcpConnection); logger.info('Session Service initialized successfully'); } catch (error) { logger.error('Failed to initialize Session Service', { error }); throw error; } } // Create new session async createSession(type, baseData, requestData) { try { const sessionData = { ...baseData, request: requestData, totalCost: 0, duration: 0, status: 'processing' }; let session; switch (type) { case 'chat': session = new this.ChatSessionModel(sessionData); break; case 'compare': session = new this.CompareSessionModel(sessionData); break; case 'brains': session = new this.BrainsSessionModel(sessionData); break; case 'models': session = new this.ModelsSessionModel(sessionData); break; default: throw new Error(`Unknown session type: ${type}`); } await session.save(); logger.debug('Session created', { sessionId: session._id, type, userId: baseData.userId }); return session._id.toString(); } catch (error) { logger.error('Failed to create session', { type, error }); throw error; } } // Update session with model interaction async addModelInteraction(sessionId, type, interactionData) { try { let updateQuery; switch (type) { case 'chat': updateQuery = { modelInteraction: interactionData, totalCost: interactionData.cost, $inc: { 'balanceUpdate.attempted': 0 } // Prepare for balance update }; break; case 'compare': case 'brains': // 🚀 ENHANCED: Calculate aggregated stats with new fields const statsIncrement = { 'aggregatedStats.totalInputTokens': interactionData.inputTokens || 0, 'aggregatedStats.totalOutputTokens': interactionData.outputTokens || 0 }; // NEW: Add thinking tokens aggregation if (interactionData.thinkingTokens) { statsIncrement['aggregatedStats.totalThinkingTokens'] = interactionData.thinkingTokens; } // NEW: Add total tokens aggregation if (interactionData.totalTokens) { statsIncrement['aggregatedStats.totalTokens'] = interactionData.totalTokens; } // NEW: Add cost breakdown aggregation if (interactionData.inputCost) { statsIncrement['aggregatedStats.totalInputCost'] = interactionData.inputCost; } if (interactionData.outputCost) { statsIncrement['aggregatedStats.totalOutputCost'] = interactionData.outputCost; } if (interactionData.thinkingCost) { statsIncrement['aggregatedStats.totalThinkingCost'] = interactionData.thinkingCost; } // NEW: Add latency aggregation for performance tracking if (interactionData.latency) { statsIncrement['aggregatedStats.totalLatency'] = interactionData.latency; } updateQuery = { $push: { modelInteractions: interactionData }, $inc: statsIncrement }; // Track success/failure counts if (interactionData.success) { updateQuery.$inc['aggregatedStats.successfulModels'] = 1; } else { updateQuery.$inc['aggregatedStats.failedModels'] = 1; } // NEW: Track feature usage counts if (interactionData.webSearchUsed) { updateQuery.$inc['aggregatedStats.webSearchUsageCount'] = 1; } if (interactionData.reasoningUsed) { updateQuery.$inc['aggregatedStats.reasoningUsageCount'] = 1; } // NEW: Track provider usage if (interactionData.provider) { updateQuery.$inc[`aggregatedStats.providerUsage.${interactionData.provider}`] = 1; } break; case 'models': updateQuery = { response: { success: interactionData.success, errorMessage: interactionData.errorMessage }, totalCost: 0, // Models endpoint is free duration: interactionData.latency }; break; default: throw new Error(`Unknown session type: ${type}`); } const Model = this.getModelByType(type); await Model.findByIdAndUpdate(sessionId, updateQuery); logger.debug('🚀 Enhanced model interaction added to session (FIXED: no double cost counting)', { sessionId, type, modelId: interactionData.modelId, provider: interactionData.provider, cost: interactionData.cost, totalTokens: interactionData.totalTokens, thinkingTokens: interactionData.thinkingTokens, latency: interactionData.latency, webSearchUsed: interactionData.webSearchUsed, reasoningUsed: interactionData.reasoningUsed, enhancedFields: !!(interactionData.thinkingTokens || interactionData.inputCost), totalCostHandledSeparately: type === 'brains' || type === 'compare' }); } catch (error) { logger.error('Failed to add model interaction', { sessionId, type, error }); throw error; } } // Update session total cost async updateSessionCost(sessionId, type, totalCost) { try { const Model = this.getModelByType(type); const updateResult = await Model.findByIdAndUpdate(sessionId, { totalCost }, { new: true } // Return updated document for verification ); logger.info('🚀 Session totalCost set (authoritative - will NOT be incremented by individual interactions)', { sessionId, type, totalCost, actualSavedCost: updateResult?.totalCost, costVerified: updateResult?.totalCost === totalCost }); } catch (error) { logger.error('Failed to update session cost', { sessionId, type, totalCost, error }); throw error; } } // Update session with balance information async updateSessionBalance(sessionId, type, balanceResult) { try { const updateQuery = { 'balanceUpdate.attempted': true, 'balanceUpdate.success': balanceResult.success, 'balanceUpdate.errorMessage': balanceResult.errorMessage, 'balanceUpdate.transactionId': balanceResult.transactionId, 'balanceUpdate.balanceAfter': balanceResult.balanceAfter }; const Model = this.getModelByType(type); await Model.findByIdAndUpdate(sessionId, updateQuery); logger.debug('Session balance updated', { sessionId, type, success: balanceResult.success }); } catch (error) { logger.error('Failed to update session balance', { sessionId, type, error }); throw error; } } calculateAggregatedStats(interactions) { const successful = interactions.filter(i => i.success === true); const failed = interactions.filter(i => i.success === false); if (successful.length === 0) { return { totalInputTokens: 0, totalOutputTokens: 0, totalThinkingTokens: 0, totalTokens: 0, averageLatency: 0, minLatency: 0, maxLatency: 0, successfulModels: 0, failedModels: failed.length, successRate: 0, costBreakdown: { totalInputCost: 0, totalOutputCost: 0, totalThinkingCost: 0 } }; } const stats = { totalInputTokens: successful.reduce((sum, i) => sum + (i.inputTokens || 0), 0), totalOutputTokens: successful.reduce((sum, i) => sum + (i.outputTokens || 0), 0), totalThinkingTokens: successful.reduce((sum, i) => sum + (i.thinkingTokens || 0), 0), totalTokens: successful.reduce((sum, i) => sum + (i.inputTokens || 0) + (i.outputTokens || 0) + (i.thinkingTokens || 0), 0), averageLatency: Math.round(successful.reduce((sum, i) => sum + (i.latency || 0), 0) / successful.length), minLatency: Math.min(...successful.map(i => i.latency || 0)), maxLatency: Math.max(...successful.map(i => i.latency || 0)), successfulModels: successful.length, failedModels: failed.length, successRate: Math.round((successful.length / interactions.length) * 100), // 🆕 Enhanced cost breakdown aggregation costBreakdown: { totalInputCost: successful.reduce((sum, i) => sum + (i.costBreakdown?.inputCost || 0), 0), totalOutputCost: successful.reduce((sum, i) => sum + (i.costBreakdown?.outputCost || 0), 0), totalThinkingCost: successful.reduce((sum, i) => sum + (i.costBreakdown?.thinkingCost || 0), 0) } }; return stats; } // Complete session async completeSession(sessionId, type, duration, status = 'success') { try { let updateQuery = { duration, status }; // Calculate aggregated stats for multi-model sessions if (type === 'compare' || type === 'brains') { const session = await this.getModelByType(type).findById(sessionId); if (session?.modelInteractions?.length > 0) { const aggregatedStats = this.calculateAggregatedStats(session.modelInteractions); updateQuery.aggregatedStats = aggregatedStats; // 🚀 COST VERIFICATION: Ensure totalCost wasn't accidentally modified const calculatedCostFromInteractions = session.modelInteractions.reduce((sum, i) => sum + (i.cost || 0), 0); const currentTotalCost = session.totalCost || 0; logger.info('🚀 Cost verification during session completion', { sessionId, type, currentTotalCost, calculatedCostFromInteractions, discrepancy: Math.abs(currentTotalCost - calculatedCostFromInteractions), interactionCount: session.modelInteractions.length, costConsistencyCheck: Math.abs(currentTotalCost - calculatedCostFromInteractions) < 0.001 ? 'PASS' : 'FAIL' }); // Don't modify totalCost here - it should remain as set by updateSessionCost } } const Model = this.getModelByType(type); const finalSession = await Model.findByIdAndUpdate(sessionId, updateQuery, { new: true }); logger.info('Session completed', { sessionId, type, duration: `${duration}ms`, status, finalTotalCost: finalSession?.totalCost }); } catch (error) { logger.error('Failed to complete session', { sessionId, type, error }); throw error; } } // Get session by ID async getSession(sessionId, type) { try { const Model = this.getModelByType(type); return await Model.findById(sessionId).lean(); } catch (error) { logger.error('Failed to get session', { sessionId, type, error }); return null; } } // Get user sessions with pagination async getUserSessions(userId, type, limit = 20, skip = 0) { try { const query = { userId }; if (type) { const Model = this.getModelByType(type); return await Model .find(query) .sort({ createdAt: -1 }) .limit(limit) .skip(skip) .lean(); } else { // Get from all session types const [chatSessions, compareSessions, brainsSessions, modelsSessions] = await Promise.all([ this.ChatSessionModel.find(query).sort({ createdAt: -1 }).limit(limit).skip(skip).lean(), this.CompareSessionModel.find(query).sort({ createdAt: -1 }).limit(limit).skip(skip).lean(), this.BrainsSessionModel.find(query).sort({ createdAt: -1 }).limit(limit).skip(skip).lean(), this.ModelsSessionModel.find(query).sort({ createdAt: -1 }).limit(limit).skip(skip).lean() ]); return [...chatSessions, ...compareSessions, ...brainsSessions, ...modelsSessions] .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .slice(0, limit); } } catch (error) { logger.error('Failed to get user sessions', { userId, type, error }); return []; } } // Get session analytics async getSessionAnalytics(userId, days = 30) { try { const dateFilter = { createdAt: { $gte: new Date(Date.now() - days * 24 * 60 * 60 * 1000) } }; const query = { userId, ...dateFilter }; const [chatStats, compareStats, brainsStats, modelsStats] = await Promise.all([ this.ChatSessionModel.aggregate([ { $match: query }, { $group: { _id: null, count: { $sum: 1 }, totalCost: { $sum: '$totalCost' }, avgDuration: { $avg: '$duration' }, successCount: { $sum: { $cond: [{ $eq: ['$status', 'success'] }, 1, 0] } } } } ]), this.CompareSessionModel.aggregate([ { $match: query }, { $group: { _id: null, count: { $sum: 1 }, totalCost: { $sum: '$totalCost' }, avgDuration: { $avg: '$duration' }, successCount: { $sum: { $cond: [{ $eq: ['$status', 'success'] }, 1, 0] } } } } ]), this.BrainsSessionModel.aggregate([ { $match: query }, { $group: { _id: null, count: { $sum: 1 }, totalCost: { $sum: '$totalCost' }, avgDuration: { $avg: '$duration' }, successCount: { $sum: { $cond: [{ $eq: ['$status', 'success'] }, 1, 0] } } } } ]), this.ModelsSessionModel.aggregate([ { $match: query }, { $group: { _id: null, count: { $sum: 1 }, totalCost: { $sum: '$totalCost' }, avgDuration: { $avg: '$duration' }, successCount: { $sum: { $cond: [{ $eq: ['$status', 'success'] }, 1, 0] } } } } ]) ]); return { chat: chatStats[0] || { count: 0, totalCost: 0, avgDuration: 0, successCount: 0 }, compare: compareStats[0] || { count: 0, totalCost: 0, avgDuration: 0, successCount: 0 }, brains: brainsStats[0] || { count: 0, totalCost: 0, avgDuration: 0, successCount: 0 }, models: modelsStats[0] || { count: 0, totalCost: 0, avgDuration: 0, successCount: 0 } }; } catch (error) { logger.error('Failed to get session analytics', { userId, error }); return null; } } getModelByType(type) { switch (type) { case 'chat': return this.ChatSessionModel; case 'compare': return this.CompareSessionModel; case 'brains': return this.BrainsSessionModel; case 'models': return this.ModelsSessionModel; default: throw new Error(`Unknown session type: ${type}`); } } } export const sessionService = SessionService.getInstance(); //# sourceMappingURL=SessionService.js.map