import { Repository, FindOptionsWhere, LessThan, MoreThanOrEqual } from 'typeorm'; import { AIModel, AIConversation, AIMessage, AIPrompt, AIUsageLog, AITenantQuota } from '../entities'; export interface ConversationFilters { userId?: string; modelId?: string; status?: string; startDate?: Date; endDate?: Date; } export class AIService { constructor( private readonly modelRepository: Repository, private readonly conversationRepository: Repository, private readonly messageRepository: Repository, private readonly promptRepository: Repository, private readonly usageLogRepository: Repository, private readonly quotaRepository: Repository ) {} // ============================================ // MODELS // ============================================ async findAllModels(): Promise { return this.modelRepository.find({ where: { isActive: true }, order: { provider: 'ASC', name: 'ASC' }, }); } async findModel(id: string): Promise { return this.modelRepository.findOne({ where: { id } }); } async findModelByCode(code: string): Promise { return this.modelRepository.findOne({ where: { code } }); } async findModelsByProvider(provider: string): Promise { return this.modelRepository.find({ where: { provider: provider as any, isActive: true }, order: { name: 'ASC' }, }); } async findModelsByType(modelType: string): Promise { return this.modelRepository.find({ where: { modelType: modelType as any, isActive: true }, order: { name: 'ASC' }, }); } // ============================================ // PROMPTS // ============================================ async findAllPrompts(tenantId?: string): Promise { if (tenantId) { return this.promptRepository.find({ where: [{ tenantId, isActive: true }, { isSystem: true, isActive: true }], order: { category: 'ASC', name: 'ASC' }, }); } return this.promptRepository.find({ where: { isActive: true }, order: { category: 'ASC', name: 'ASC' }, }); } async findPrompt(id: string): Promise { return this.promptRepository.findOne({ where: { id } }); } async findPromptByCode(code: string, tenantId?: string): Promise { if (tenantId) { // Try tenant-specific first, then system prompt const tenantPrompt = await this.promptRepository.findOne({ where: { code, tenantId, isActive: true }, }); if (tenantPrompt) return tenantPrompt; return this.promptRepository.findOne({ where: { code, isSystem: true, isActive: true }, }); } return this.promptRepository.findOne({ where: { code, isActive: true } }); } async createPrompt( tenantId: string, data: Partial, createdBy?: string ): Promise { const prompt = this.promptRepository.create({ ...data, tenantId, createdBy, version: 1, }); return this.promptRepository.save(prompt); } async updatePrompt( id: string, data: Partial, updatedBy?: string ): Promise { const prompt = await this.findPrompt(id); if (!prompt) return null; if (prompt.isSystem) { throw new Error('Cannot update system prompts'); } Object.assign(prompt, data, { updatedBy, version: prompt.version + 1 }); return this.promptRepository.save(prompt); } async incrementPromptUsage(id: string): Promise { await this.promptRepository .createQueryBuilder() .update() .set({ usageCount: () => 'usage_count + 1', }) .where('id = :id', { id }) .execute(); } // ============================================ // CONVERSATIONS // ============================================ async findConversations( tenantId: string, filters: ConversationFilters = {}, limit: number = 50 ): Promise { const where: FindOptionsWhere = { tenantId }; if (filters.userId) where.userId = filters.userId; if (filters.modelId) where.modelId = filters.modelId; if (filters.status) where.status = filters.status as any; return this.conversationRepository.find({ where, order: { updatedAt: 'DESC' }, take: limit, }); } async findConversation(id: string): Promise { return this.conversationRepository.findOne({ where: { id }, relations: ['messages'], }); } async findUserConversations( tenantId: string, userId: string, limit: number = 20 ): Promise { return this.conversationRepository.find({ where: { tenantId, userId }, order: { updatedAt: 'DESC' }, take: limit, }); } async createConversation( tenantId: string, userId: string, data: Partial ): Promise { const conversation = this.conversationRepository.create({ ...data, tenantId, userId, status: 'active', }); return this.conversationRepository.save(conversation); } async updateConversation( id: string, data: Partial ): Promise { const conversation = await this.conversationRepository.findOne({ where: { id } }); if (!conversation) return null; Object.assign(conversation, data); return this.conversationRepository.save(conversation); } async archiveConversation(id: string): Promise { const result = await this.conversationRepository.update(id, { status: 'archived' }); return (result.affected ?? 0) > 0; } // ============================================ // MESSAGES // ============================================ async findMessages(conversationId: string): Promise { return this.messageRepository.find({ where: { conversationId }, order: { createdAt: 'ASC' }, }); } async addMessage(conversationId: string, data: Partial): Promise { const message = this.messageRepository.create({ ...data, conversationId, }); const savedMessage = await this.messageRepository.save(message); // Update conversation await this.conversationRepository .createQueryBuilder() .update() .set({ messageCount: () => 'message_count + 1', totalTokens: () => `total_tokens + ${data.totalTokens || 0}`, updatedAt: new Date(), }) .where('id = :id', { id: conversationId }) .execute(); return savedMessage; } async getConversationTokenCount(conversationId: string): Promise { const result = await this.messageRepository .createQueryBuilder('message') .select('SUM(message.total_tokens)', 'total') .where('message.conversation_id = :conversationId', { conversationId }) .getRawOne(); return parseInt(result?.total) || 0; } // ============================================ // USAGE & QUOTAS // ============================================ async logUsage(tenantId: string, data: Partial): Promise { const log = this.usageLogRepository.create({ ...data, tenantId, }); return this.usageLogRepository.save(log); } async getUsageStats( tenantId: string, startDate: Date, endDate: Date ): Promise<{ totalRequests: number; totalInputTokens: number; totalOutputTokens: number; totalCost: number; byModel: Record; }> { const stats = await this.usageLogRepository .createQueryBuilder('log') .select('COUNT(*)', 'totalRequests') .addSelect('SUM(log.input_tokens)', 'totalInputTokens') .addSelect('SUM(log.output_tokens)', 'totalOutputTokens') .addSelect('SUM(log.cost_usd)', 'totalCost') .where('log.tenant_id = :tenantId', { tenantId }) .andWhere('log.created_at BETWEEN :startDate AND :endDate', { startDate, endDate }) .getRawOne(); const byModelStats = await this.usageLogRepository .createQueryBuilder('log') .select('log.model_id', 'modelId') .addSelect('COUNT(*)', 'requests') .addSelect('SUM(log.input_tokens + log.output_tokens)', 'tokens') .addSelect('SUM(log.cost_usd)', 'cost') .where('log.tenant_id = :tenantId', { tenantId }) .andWhere('log.created_at BETWEEN :startDate AND :endDate', { startDate, endDate }) .groupBy('log.model_id') .getRawMany(); const byModel: Record = {}; for (const stat of byModelStats) { byModel[stat.modelId] = { requests: parseInt(stat.requests) || 0, tokens: parseInt(stat.tokens) || 0, cost: parseFloat(stat.cost) || 0, }; } return { totalRequests: parseInt(stats?.totalRequests) || 0, totalInputTokens: parseInt(stats?.totalInputTokens) || 0, totalOutputTokens: parseInt(stats?.totalOutputTokens) || 0, totalCost: parseFloat(stats?.totalCost) || 0, byModel, }; } async getTenantQuota(tenantId: string): Promise { return this.quotaRepository.findOne({ where: { tenantId } }); } async updateTenantQuota( tenantId: string, data: Partial ): Promise { let quota = await this.getTenantQuota(tenantId); if (!quota) { quota = this.quotaRepository.create({ tenantId, ...data, }); } else { Object.assign(quota, data); } return this.quotaRepository.save(quota); } async incrementQuotaUsage( tenantId: string, requestCount: number, tokenCount: number, costUsd: number ): Promise { await this.quotaRepository .createQueryBuilder() .update() .set({ currentRequests: () => `current_requests + ${requestCount}`, currentTokens: () => `current_tokens + ${tokenCount}`, currentCost: () => `current_cost + ${costUsd}`, }) .where('tenant_id = :tenantId', { tenantId }) .execute(); } async checkQuotaAvailable(tenantId: string): Promise<{ available: boolean; reason?: string; }> { const quota = await this.getTenantQuota(tenantId); if (!quota) return { available: true }; if (quota.monthlyRequestLimit && quota.currentRequests >= quota.monthlyRequestLimit) { return { available: false, reason: 'Monthly request limit reached' }; } if (quota.monthlyTokenLimit && quota.currentTokens >= quota.monthlyTokenLimit) { return { available: false, reason: 'Monthly token limit reached' }; } if (quota.monthlyCostLimit && quota.currentCost >= quota.monthlyCostLimit) { return { available: false, reason: 'Monthly spend limit reached' }; } return { available: true }; } async resetMonthlyQuotas(): Promise { const result = await this.quotaRepository.update( {}, { currentRequests: 0, currentTokens: 0, currentCost: 0, } ); return result.affected ?? 0; } }