tryaii-mcp-server
Version:
TryAII MCP Server - 15+ AI models with comparison, cost tracking, and collective intelligence
414 lines • 19.2 kB
JavaScript
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