// AI Recommendation Service - Personalized content recommendation system import { RecommendationResult, ContentInfo, AIProvider, AIResponse, AIServiceConfig, AIServiceError, BatchProcessingOptions } from '../types/ai-types.uts' // 用户行为数据 type UserBehavior = { userId: string contentId: string actionType: 'view' | 'like' | 'share' | 'comment' | 'save' | 'skip' timestamp: number duration?: number // 阅读时长(秒) deviceType?: string location?: string context?: UTSJSONObject } // 用户画像 type UserProfile = { userId: string demographics: { age?: number gender?: string location?: string education?: string occupation?: string } interests: { categories: Record // 分类偏好权重 keywords: Record // 关键词偏好权重 sources: Record // 来源偏好权重 languages: Record // 语言偏好权重 } behavior: { readingTime: number // 平均阅读时长 activeHours: number[] // 活跃时段 preferredContentLength: 'short' | 'medium' | 'long' engagementRate: number // 互动率 } preferences: { newsStyle: 'breaking' | 'analysis' | 'opinion' | 'feature' updateFrequency: 'realtime' | 'hourly' | 'daily' | 'weekly' contentFreshness: number // 内容新鲜度偏好 (0-1) diversityLevel: number // 多样性偏好 (0-1) } lastUpdated: number } // 推荐算法类型 type RecommendationAlgorithm = | 'collaborative_filtering' | 'content_based' | 'hybrid' | 'trending' | 'similarity' | 'ml_based' // 推荐配置 type RecommendationConfig = { algorithm: RecommendationAlgorithm maxResults: number diversityWeight: number // 多样性权重 freshnessWeight: number // 新鲜度权重 personalizedWeight: number // 个性化权重 qualityThreshold: number // 质量阈值 excludeViewed: boolean // 排除已浏览内容 includeCategories?: string[] excludeCategories?: string[] timeRange?: number // 时间范围(小时) } // 推荐上下文 type RecommendationContext = { currentTime: number userLocation?: string deviceType?: string sessionDuration?: number recentViews: string[] // 最近浏览的内容ID currentCategory?: string searchQuery?: string } // 相似度计算结果 type SimilarityResult = { contentId: string similarity: number reasons: string[] } // 推荐统计 type RecommendationStats = { totalRecommendations: number clickThroughRate: number averageEngagementTime: number algorithmPerformance: Record categoryDistribution: Record userSatisfactionScore: number } /** * AI推荐服务类 * 提供个性化内容推荐,支持多种推荐算法和实时优化 */ export class AIRecommendationService { private config: AIServiceConfig private userProfiles: Map = new Map() private userBehaviors: Map = new Map() private contentSimilarityCache: Map = new Map() private stats: RecommendationStats = { totalRecommendations: 0, clickThroughRate: 0, averageEngagementTime: 0, algorithmPerformance: {} as Record, categoryDistribution: {}, userSatisfactionScore: 0 } constructor(config: AIServiceConfig) { this.config = config this.initializeStats() } /** * 获取个性化推荐 * @param userId 用户ID * @param availableContent 可用内容列表 * @param config 推荐配置 * @param context 推荐上下文 */ async getPersonalizedRecommendations( userId: string, availableContent: ContentInfo[], config: RecommendationConfig = { algorithm: 'hybrid', maxResults: 10, diversityWeight: 0.3, freshnessWeight: 0.2, personalizedWeight: 0.5, qualityThreshold: 0.7, excludeViewed: true }, context: RecommendationContext = { currentTime: Date.now(), recentViews: [] } ): Promise> { try { this.stats.totalRecommendations++ // 获取或创建用户画像 const userProfile = await this.getUserProfile(userId) // 过滤内容 let filteredContent = this.filterContent(availableContent, config, context, userProfile) // 根据算法生成推荐 let recommendations: RecommendationResult[] = [] switch (config.algorithm) { case 'collaborative_filtering': recommendations = await this.collaborativeFiltering(userId, filteredContent, config, userProfile) break case 'content_based': recommendations = await this.contentBasedFiltering(userId, filteredContent, config, userProfile) break case 'hybrid': recommendations = await this.hybridRecommendation(userId, filteredContent, config, userProfile, context) break case 'trending': recommendations = await this.trendingRecommendation(filteredContent, config, context) break case 'similarity': recommendations = await this.similarityBasedRecommendation(userId, filteredContent, config, context) break case 'ml_based': recommendations = await this.mlBasedRecommendation(userId, filteredContent, config, userProfile) break default: recommendations = await this.hybridRecommendation(userId, filteredContent, config, userProfile, context) } // 应用多样性和质量过滤 recommendations = this.applyDiversityAndQuality(recommendations, config) // 排序和截取 recommendations = recommendations .sort((a, b) => b.score - a.score) .slice(0, config.maxResults) .map((rec, index) => ({ ...rec, position: index + 1 })) // 更新统计 this.updateRecommendationStats(config.algorithm, recommendations) return { success: true, data: recommendations, provider: 'ai_recommendation' } } catch (error) { const aiError: AIServiceError = { code: 'RECOMMENDATION_ERROR', message: error.message || 'Failed to generate recommendations', retryable: false } return { success: false, error: aiError.message, errorCode: aiError.code } } } /** * 记录用户行为 * @param behavior 用户行为数据 */ async recordUserBehavior(behavior: UserBehavior): Promise { const userId = behavior.userId if (!this.userBehaviors.has(userId)) { this.userBehaviors.set(userId, []) } const behaviors = this.userBehaviors.get(userId)! behaviors.push(behavior) // 保持最近1000条行为记录 if (behaviors.length > 1000) { behaviors.splice(0, behaviors.length - 1000) } // 异步更新用户画像 this.updateUserProfile(userId, behavior) } /** * 获取相似内容推荐 * @param contentId 基准内容ID * @param availableContent 可用内容列表 * @param maxResults 最大结果数 */ async getSimilarContent( contentId: string, availableContent: ContentInfo[], maxResults: number = 5 ): Promise> { try { const baseContent = availableContent.find(c => c.id === contentId) if (!baseContent) { throw new Error('Base content not found') } // 检查缓存 const cached = this.contentSimilarityCache.get(contentId) if (cached) { const recommendations = cached.slice(0, maxResults).map((sim, index) => ({ contentId: sim.contentId, userId: '', score: sim.similarity, reason: sim.reasons.join(', '), algorithm: 'similarity', contextFactors: { similarityReasons: sim.reasons }, recommendationType: 'similar' as const, position: index + 1, createdAt: Date.now() })) return { success: true, data: recommendations } } // 计算相似度 const similarities = await this.calculateContentSimilarities(baseContent, availableContent) // 缓存结果 this.contentSimilarityCache.set(contentId, similarities) // 生成推荐结果 const recommendations = similarities.slice(0, maxResults).map((sim, index) => ({ contentId: sim.contentId, userId: '', score: sim.similarity, reason: sim.reasons.join(', '), algorithm: 'similarity', contextFactors: { similarityReasons: sim.reasons }, recommendationType: 'similar' as const, position: index + 1, createdAt: Date.now() })) return { success: true, data: recommendations } } catch (error) { return { success: false, error: error.message || 'Failed to get similar content' } } } /** * 获取热门推荐 * @param availableContent 可用内容列表 * @param timeRange 时间范围(小时) * @param maxResults 最大结果数 */ async getTrendingRecommendations( availableContent: ContentInfo[], timeRange: number = 24, maxResults: number = 10 ): Promise> { try { const cutoffTime = Date.now() - (timeRange * 60 * 60 * 1000) // 计算热门度分数 const trendingScores = availableContent.map(content => { // 时间衰减因子 const ageHours = (Date.now() - content.publishedAt) / (1000 * 60 * 60) const timeFactor = Math.exp(-ageHours / 24) // 24小时半衰期 // 互动分数 const engagementScore = (content.viewCount * 1 + content.likeCount * 3 + content.shareCount * 5) / 100 // 质量分数 const qualityScore = content.quality || 0.5 // 综合分数 const score = (engagementScore * 0.5 + qualityScore * 0.3 + timeFactor * 0.2) return { contentId: content.id, score, engagementScore, timeFactor, qualityScore } }) // 排序并生成推荐 const recommendations = trendingScores .sort((a, b) => b.score - a.score) .slice(0, maxResults) .map((item, index) => ({ contentId: item.contentId, userId: '', score: item.score, reason: `热门内容 - 互动度: ${item.engagementScore.toFixed(2)}, 新鲜度: ${item.timeFactor.toFixed(2)}`, algorithm: 'trending', contextFactors: { engagementScore: item.engagementScore, timeFactor: item.timeFactor, qualityScore: item.qualityScore }, recommendationType: 'trending' as const, position: index + 1, createdAt: Date.now() })) return { success: true, data: recommendations } } catch (error) { return { success: false, error: error.message || 'Failed to get trending recommendations' } } } /** * 批量生成推荐 * @param userIds 用户ID列表 * @param availableContent 可用内容列表 * @param config 推荐配置 * @param batchOptions 批处理选项 */ async generateBatchRecommendations( userIds: string[], availableContent: ContentInfo[], config: RecommendationConfig, batchOptions: BatchProcessingOptions = { batchSize: 10, concurrency: 3, retryCount: 2, delayMs: 100 } ): Promise>> { try { const results: Record = {} const batches = this.createBatches(userIds, batchOptions.batchSize) for (let i = 0; i < batches.length; i++) { const batch = batches[i] const batchPromises = batch.map(async (userId) => { try { const response = await this.getPersonalizedRecommendations(userId, availableContent, config) if (response.success && response.data) { return { userId, recommendations: response.data } } throw new Error(response.error || 'Recommendation failed') } catch (error) { if (batchOptions.onError) { batchOptions.onError(error, userId) } return { userId, recommendations: [] } } }) const batchResults = await Promise.allSettled(batchPromises) for (const result of batchResults) { if (result.status === 'fulfilled') { results[result.value.userId] = result.value.recommendations } } // 进度回调 if (batchOptions.onProgress) { batchOptions.onProgress(Object.keys(results).length, userIds.length) } // 批次间延迟 if (i < batches.length - 1 && batchOptions.delayMs > 0) { await this.delay(batchOptions.delayMs) } } return { success: true, data: results } } catch (error) { return { success: false, error: error.message || 'Batch recommendation generation failed' } } } /** * 获取推荐统计 */ getRecommendationStatistics(): RecommendationStats { return { ...this.stats } } /** * 获取用户画像 * @param userId 用户ID */ async getUserProfile(userId: string): Promise { let profile = this.userProfiles.get(userId) if (!profile) { profile = this.createDefaultUserProfile(userId) this.userProfiles.set(userId, profile) } return profile } /** * 更新用户画像 * @param userId 用户ID * @param behavior 用户行为 */ private async updateUserProfile(userId: string, behavior: UserBehavior): Promise { const profile = await this.getUserProfile(userId) // 更新行为统计 const behaviors = this.userBehaviors.get(userId) || [] // 计算平均阅读时长 const readingTimes = behaviors .filter(b => b.duration && b.duration > 0) .map(b => b.duration!) if (readingTimes.length > 0) { profile.behavior.readingTime = readingTimes.reduce((sum, time) => sum + time, 0) / readingTimes.length } // 更新活跃时段 const hour = new Date(behavior.timestamp).getHours() if (!profile.behavior.activeHours.includes(hour)) { profile.behavior.activeHours.push(hour) } // 更新互动率 const engagementActions = behaviors.filter(b => ['like', 'share', 'comment', 'save'].includes(b.actionType) ).length const totalActions = behaviors.length profile.behavior.engagementRate = totalActions > 0 ? engagementActions / totalActions : 0 // 根据行为类型更新偏好权重 if (behavior.actionType === 'like' || behavior.actionType === 'share') { // 增加对应内容的权重(这里需要内容信息,暂时跳过) } profile.lastUpdated = Date.now() } // Private methods private async collaborativeFiltering( userId: string, content: ContentInfo[], config: RecommendationConfig, userProfile: UserProfile ): Promise { // 简化的协同过滤实现 const recommendations: RecommendationResult[] = [] // 找到相似用户(基于兴趣分类相似度) const similarUsers = await this.findSimilarUsers(userId, userProfile) // 获取相似用户喜欢的内容 for (const content_item of content) { let score = 0 let voterCount = 0 for (const similarUser of similarUsers) { const behaviors = this.userBehaviors.get(similarUser.userId) || [] const interaction = behaviors.find(b => b.contentId === content_item.id) if (interaction) { const weight = similarUser.similarity if (interaction.actionType === 'like') score += 0.8 * weight else if (interaction.actionType === 'share') score += 1.0 * weight else if (interaction.actionType === 'view') score += 0.3 * weight voterCount++ } } if (voterCount > 0) { score /= voterCount recommendations.push({ contentId: content_item.id, userId, score, reason: `基于${voterCount}个相似用户的偏好`, algorithm: 'collaborative_filtering', contextFactors: { voterCount, similarUserCount: similarUsers.length }, recommendationType: 'personalized', position: 0, createdAt: Date.now() }) } } return recommendations } private async contentBasedFiltering( userId: string, content: ContentInfo[], config: RecommendationConfig, userProfile: UserProfile ): Promise { const recommendations: RecommendationResult[] = [] for (const item of content) { let score = 0 const reasons: string[] = [] // 分类匹配 if (item.categoryId && userProfile.interests.categories[item.categoryId]) { const categoryScore = userProfile.interests.categories[item.categoryId] score += categoryScore * 0.4 reasons.push(`分类偏好: ${categoryScore.toFixed(2)}`) } // 关键词匹配 let keywordScore = 0 let keywordCount = 0 for (const keyword of item.keywords) { if (userProfile.interests.keywords[keyword]) { keywordScore += userProfile.interests.keywords[keyword] keywordCount++ } } if (keywordCount > 0) { keywordScore /= keywordCount score += keywordScore * 0.3 reasons.push(`关键词匹配: ${keywordCount}个`) } // 质量分数 score += (item.quality || 0.5) * 0.2 // 时效性 const ageHours = (Date.now() - item.publishedAt) / (1000 * 60 * 60) const freshness = Math.exp(-ageHours / 24) score += freshness * 0.1 reasons.push(`新鲜度: ${freshness.toFixed(2)}`) recommendations.push({ contentId: item.id, userId, score, reason: reasons.join(', '), algorithm: 'content_based', contextFactors: { categoryScore: userProfile.interests.categories[item.categoryId || ''] || 0, keywordScore, qualityScore: item.quality || 0.5, freshness }, recommendationType: 'personalized', position: 0, createdAt: Date.now() }) } return recommendations } private async hybridRecommendation( userId: string, content: ContentInfo[], config: RecommendationConfig, userProfile: UserProfile, context: RecommendationContext ): Promise { // 结合多种算法 const [ collaborativeRecs, contentBasedRecs, trendingRecs ] = await Promise.all([ this.collaborativeFiltering(userId, content, config, userProfile), this.contentBasedFiltering(userId, content, config, userProfile), this.trendingRecommendation(content, config, context) ]) // 合并和重新评分 const hybridRecs: RecommendationResult[] = [] const contentScores: Record = {} // 收集各算法的分数 collaborativeRecs.forEach(rec => { if (!contentScores[rec.contentId]) contentScores[rec.contentId] = { collaborative: 0, contentBased: 0, trending: 0 } contentScores[rec.contentId].collaborative = rec.score }) contentBasedRecs.forEach(rec => { if (!contentScores[rec.contentId]) contentScores[rec.contentId] = { collaborative: 0, contentBased: 0, trending: 0 } contentScores[rec.contentId].contentBased = rec.score }) trendingRecs.forEach(rec => { if (!contentScores[rec.contentId]) contentScores[rec.contentId] = { collaborative: 0, contentBased: 0, trending: 0 } contentScores[rec.contentId].trending = rec.score }) // 计算混合分数 for (const [contentId, scores] of Object.entries(contentScores)) { const hybridScore = ( scores.collaborative * 0.4 + scores.contentBased * 0.4 + scores.trending * 0.2 ) hybridRecs.push({ contentId, userId, score: hybridScore, reason: `混合算法 - 协同: ${scores.collaborative.toFixed(2)}, 内容: ${scores.contentBased.toFixed(2)}, 热门: ${scores.trending.toFixed(2)}`, algorithm: 'hybrid', contextFactors: scores, recommendationType: 'personalized', position: 0, createdAt: Date.now() }) } return hybridRecs } private async trendingRecommendation( content: ContentInfo[], config: RecommendationConfig, context: RecommendationContext ): Promise { const response = await this.getTrendingRecommendations(content, config.timeRange || 24, content.length) return response.data || [] } private async similarityBasedRecommendation( userId: string, content: ContentInfo[], config: RecommendationConfig, context: RecommendationContext ): Promise { if (!context.recentViews || context.recentViews.length === 0) { return [] } const recommendations: RecommendationResult[] = [] // 基于最近浏览的内容推荐相似内容 for (const viewedContentId of context.recentViews.slice(-3)) { const response = await this.getSimilarContent(viewedContentId, content, 3) if (response.success && response.data) { recommendations.push(...response.data.map(rec => ({ ...rec, userId, algorithm: 'similarity' as const, recommendationType: 'similar' as const }))) } } return recommendations } private async mlBasedRecommendation( userId: string, content: ContentInfo[], config: RecommendationConfig, userProfile: UserProfile ): Promise { // 简化的ML基础推荐,实际实现需要训练模型 const recommendations: RecommendationResult[] = [] for (const item of content) { // 特征提取 const features = this.extractFeatures(item, userProfile) // 简单的线性评分模型 const score = this.calculateMLScore(features) recommendations.push({ contentId: item.id, userId, score, reason: `ML模型预测分数: ${score.toFixed(3)}`, algorithm: 'ml_based', contextFactors: { features }, recommendationType: 'personalized', position: 0, createdAt: Date.now() }) } return recommendations } private extractFeatures(content: ContentInfo, userProfile: UserProfile): number[] { const features: number[] = [] // 内容特征 features.push(content.quality || 0.5) // 质量分数 features.push(content.viewCount / 10000) // 标准化浏览数 features.push(content.likeCount / 1000) // 标准化点赞数 features.push(content.shareCount / 100) // 标准化分享数 // 时间特征 const ageHours = (Date.now() - content.publishedAt) / (1000 * 60 * 60) features.push(Math.exp(-ageHours / 24)) // 时效性 // 用户偏好匹配 const categoryPreference = userProfile.interests.categories[content.categoryId || ''] || 0 features.push(categoryPreference) // 关键词匹配度 let keywordMatch = 0 for (const keyword of content.keywords) { keywordMatch += userProfile.interests.keywords[keyword] || 0 } features.push(keywordMatch / Math.max(content.keywords.length, 1)) return features } private calculateMLScore(features: number[]): number { // 简单的线性模型权重 const weights = [0.2, 0.1, 0.15, 0.1, 0.15, 0.2, 0.1] let score = 0 for (let i = 0; i < Math.min(features.length, weights.length); i++) { score += features[i] * weights[i] } return Math.max(0, Math.min(1, score)) } private filterContent( content: ContentInfo[], config: RecommendationConfig, context: RecommendationContext, userProfile: UserProfile ): ContentInfo[] { return content.filter(item => { // 质量过滤 if ((item.quality || 0) < config.qualityThreshold) return false // 排除已浏览 if (config.excludeViewed && context.recentViews.includes(item.id)) return false // 分类过滤 if (config.includeCategories && config.includeCategories.length > 0) { if (!item.categoryId || !config.includeCategories.includes(item.categoryId)) return false } if (config.excludeCategories && config.excludeCategories.length > 0) { if (item.categoryId && config.excludeCategories.includes(item.categoryId)) return false } // 时间范围过滤 if (config.timeRange) { const cutoffTime = Date.now() - (config.timeRange * 60 * 60 * 1000) if (item.publishedAt < cutoffTime) return false } return true }) } private applyDiversityAndQuality( recommendations: RecommendationResult[], config: RecommendationConfig ): RecommendationResult[] { if (config.diversityWeight <= 0) return recommendations const diversified: RecommendationResult[] = [] const selectedCategories: Set = new Set() // 按分数排序 const sorted = [...recommendations].sort((a, b) => b.score - a.score) for (const rec of sorted) { // 简单的多样性检查(基于内容ID的哈希) const categoryHash = rec.contentId.substring(0, 3) // 简化的分类标识 if (!selectedCategories.has(categoryHash) || diversified.length < config.maxResults / 2) { diversified.push(rec) selectedCategories.add(categoryHash) if (diversified.length >= config.maxResults) break } } return diversified } private async calculateContentSimilarities( baseContent: ContentInfo, availableContent: ContentInfo[] ): Promise { const similarities: SimilarityResult[] = [] for (const content of availableContent) { if (content.id === baseContent.id) continue let similarity = 0 const reasons: string[] = [] // 分类相似度 if (content.categoryId === baseContent.categoryId) { similarity += 0.4 reasons.push('相同分类') } // 关键词相似度 const commonKeywords = content.keywords.filter(k => baseContent.keywords.includes(k)) if (commonKeywords.length > 0) { const keywordSimilarity = commonKeywords.length / Math.max(content.keywords.length, baseContent.keywords.length) similarity += keywordSimilarity * 0.3 reasons.push(`共同关键词: ${commonKeywords.length}个`) } // 标题相似度(简化版) const titleSimilarity = this.calculateTextSimilarity(content.title, baseContent.title) similarity += titleSimilarity * 0.2 if (titleSimilarity > 0.3) { reasons.push('标题相似') } // 时间相似度 const timeDiff = Math.abs(content.publishedAt - baseContent.publishedAt) const timeHours = timeDiff / (1000 * 60 * 60) const timeSimilarity = Math.exp(-timeHours / 168) // 一周半衰期 similarity += timeSimilarity * 0.1 if (similarity > 0.1) { similarities.push({ contentId: content.id, similarity, reasons }) } } return similarities.sort((a, b) => b.similarity - a.similarity) } private calculateTextSimilarity(text1: string, text2: string): number { // 简单的文本相似度计算 const words1 = new Set(text1.toLowerCase().split(/\s+/)) const words2 = new Set(text2.toLowerCase().split(/\s+/)) const intersection = new Set([...words1].filter(x => words2.has(x))) const union = new Set([...words1, ...words2]) return intersection.size / union.size } private async findSimilarUsers(userId: string, userProfile: UserProfile): Promise> { const similarUsers: Array<{ userId: string, similarity: number }> = [] // 遍历所有其他用户(实际实现中应该有更高效的方法) for (const [otherUserId, otherProfile] of this.userProfiles.entries()) { if (otherUserId === userId) continue const similarity = this.calculateUserSimilarity(userProfile, otherProfile) if (similarity > 0.5) { similarUsers.push({ userId: otherUserId, similarity }) } } return similarUsers.sort((a, b) => b.similarity - a.similarity).slice(0, 10) } private calculateUserSimilarity(profile1: UserProfile, profile2: UserProfile): number { let similarity = 0 // 分类偏好相似度 const categories1 = Object.keys(profile1.interests.categories) const categories2 = Object.keys(profile2.interests.categories) const commonCategories = categories1.filter(c => categories2.includes(c)) if (commonCategories.length > 0) { let categoryScore = 0 for (const category of commonCategories) { const score1 = profile1.interests.categories[category] const score2 = profile2.interests.categories[category] categoryScore += 1 - Math.abs(score1 - score2) } similarity += (categoryScore / commonCategories.length) * 0.6 } // 行为相似度 const behavior1 = profile1.behavior const behavior2 = profile2.behavior const readingTimeSimilarity = 1 - Math.abs(behavior1.readingTime - behavior2.readingTime) / Math.max(behavior1.readingTime, behavior2.readingTime, 1) const engagementSimilarity = 1 - Math.abs(behavior1.engagementRate - behavior2.engagementRate) similarity += (readingTimeSimilarity + engagementSimilarity) * 0.2 return similarity } private createDefaultUserProfile(userId: string): UserProfile { return { userId, demographics: {}, interests: { categories: {}, keywords: {}, sources: {}, languages: { 'zh-CN': 1.0 } }, behavior: { readingTime: 120, // 默认2分钟 activeHours: [], preferredContentLength: 'medium', engagementRate: 0 }, preferences: { newsStyle: 'brief', updateFrequency: 'daily', contentFreshness: 0.7, diversityLevel: 0.5 }, lastUpdated: Date.now() } } private createBatches(items: T[], batchSize: number): T[][] { const batches: T[][] = [] for (let i = 0; i < items.length; i += batchSize) { batches.push(items.slice(i, i + batchSize)) } return batches } private async delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } private initializeStats(): void { const algorithms: RecommendationAlgorithm[] = [ 'collaborative_filtering', 'content_based', 'hybrid', 'trending', 'similarity', 'ml_based' ] algorithms.forEach(algorithm => { this.stats.algorithmPerformance[algorithm] = { usage: 0, successRate: 1.0, avgScore: 0.5 } }) } private updateRecommendationStats(algorithm: RecommendationAlgorithm, recommendations: RecommendationResult[]): void { this.stats.algorithmPerformance[algorithm].usage++ if (recommendations.length > 0) { const avgScore = recommendations.reduce((sum, rec) => sum + rec.score, 0) / recommendations.length const current = this.stats.algorithmPerformance[algorithm] current.avgScore = (current.avgScore * (current.usage - 1) + avgScore) / current.usage } } }