Skip to main content
Glama

YouTube Knowledge MCP

by efikuta
learning-path.ts16.3 kB
import { Logger } from 'winston'; import { YouTubeClient } from '../youtube-client.js'; import { CacheManager } from '../utils/cache.js'; import { QuotaManager } from '../utils/quota.js'; import { RobustLLMService } from '../utils/llm-service.js'; import { GenerateLearningPathParams, GenerateLearningPathSchema, LearningPath, YouTubeVideo } from '../types.js'; export class LearningPathGenerator { constructor( private youtubeClient: YouTubeClient, private cache: CacheManager, private quotaManager: QuotaManager, private llmService: RobustLLMService, private logger: Logger ) {} async execute(args: unknown): Promise<LearningPath> { const params = GenerateLearningPathSchema.parse(args); this.logger.info(`Generating learning path for: "${params.query}" (${params.targetLevel} level)`); // Generate cache key const cacheKey = `learning_path:${Buffer.from(JSON.stringify(params)).toString('base64')}`; // Check cache first const cached = await this.cache.get<LearningPath>(cacheKey); if (cached) { this.logger.info(`Returning cached learning path for: "${params.query}"`); return cached; } try { // Step 1: Search for relevant videos const searchResults = await this.searchRelevantVideos(params); // Step 2: Analyze and rank videos const rankedVideos = await this.analyzeAndRankVideos(searchResults, params); // Step 3: Generate learning sequence using LLM const learningPath = await this.generateLearningSequence(rankedVideos, params); // Step 4: Enhance with additional metadata const enhancedPath = await this.enhanceLearningPath(learningPath, params); // Cache the result await this.cache.set(cacheKey, enhancedPath, 7200); // 2 hours cache this.logger.info(`Learning path generated for "${params.query}": ${enhancedPath.videos.length} videos`); return enhancedPath; } catch (error) { this.logger.error(`Failed to generate learning path for "${params.query}":`, error); throw error; } } /** * Search for videos relevant to the learning topic */ private async searchRelevantVideos(params: GenerateLearningPathParams): Promise<YouTubeVideo[]> { const searchQueries = this.generateSearchQueries(params.query, params.targetLevel); const allVideos: YouTubeVideo[] = []; for (const query of searchQueries) { try { const searchResult = await this.youtubeClient.searchVideos({ query, maxResults: Math.ceil(params.maxVideos / searchQueries.length), order: 'relevance', videoDuration: 'medium' // Prefer medium-length educational content }); allVideos.push(...searchResult.videos); } catch (error) { this.logger.warn(`Failed to search for "${query}":`, error); } } // Remove duplicates and limit results const uniqueVideos = this.removeDuplicates(allVideos); return uniqueVideos.slice(0, params.maxVideos * 2); // Get extra for filtering } /** * Generate diverse search queries for comprehensive coverage */ private generateSearchQueries(topic: string, level: string): string[] { const baseQueries = [ `${topic} tutorial`, `learn ${topic}`, `${topic} explained`, `${topic} course` ]; const levelModifiers = { beginner: ['beginner', 'basics', 'introduction', 'getting started', 'fundamentals'], intermediate: ['intermediate', 'advanced tutorial', 'in-depth', 'comprehensive'], advanced: ['advanced', 'expert', 'professional', 'mastery', 'deep dive'] }; const modifiers = levelModifiers[level] || levelModifiers.beginner; const queries: string[] = []; // Combine base queries with level modifiers baseQueries.forEach(base => { queries.push(base); modifiers.forEach(modifier => { queries.push(`${modifier} ${base}`); queries.push(`${base} ${modifier}`); }); }); return queries.slice(0, 8); // Limit to 8 queries to manage API quota } /** * Analyze and rank videos for educational value */ private async analyzeAndRankVideos( videos: YouTubeVideo[], params: GenerateLearningPathParams ): Promise<Array<YouTubeVideo & { educationalScore: number; difficultyLevel: string }>> { const analyzedVideos = await Promise.all( videos.map(async (video) => { try { // Get basic educational metrics const educationalScore = this.calculateEducationalScore(video); const difficultyLevel = this.assessDifficultyLevel(video, params.targetLevel); return { ...video, educationalScore, difficultyLevel }; } catch (error) { this.logger.warn(`Failed to analyze video ${video.id}:`, error); return { ...video, educationalScore: 0.5, difficultyLevel: params.targetLevel }; } }) ); // Filter and sort by educational value return analyzedVideos .filter(video => video.educationalScore > 0.3) // Filter out low-quality content .sort((a, b) => b.educationalScore - a.educationalScore) .slice(0, params.maxVideos); } /** * Calculate educational score based on video metadata */ private calculateEducationalScore(video: YouTubeVideo): number { let score = 0.5; // Base score // Duration scoring (prefer 10-30 minute videos for education) const duration = this.parseDuration(video.duration || 'PT0S'); if (duration >= 600 && duration <= 1800) { // 10-30 minutes score += 0.2; } else if (duration < 300) { // Too short score -= 0.1; } // View to like ratio (engagement quality) const views = parseInt(video.viewCount || '0'); const likes = parseInt(video.likeCount || '0'); if (views > 0 && likes > 0) { const likeRatio = likes / views; if (likeRatio > 0.02) score += 0.15; // Good engagement } // Title analysis for educational keywords const educationalKeywords = [ 'tutorial', 'learn', 'explained', 'guide', 'how to', 'course', 'lesson', 'teach', 'education', 'training', 'instruction' ]; const titleLower = video.title.toLowerCase(); const keywordMatches = educationalKeywords.filter(keyword => titleLower.includes(keyword) ).length; score += Math.min(keywordMatches * 0.05, 0.15); // Channel credibility (simplified) if (video.channelTitle && video.channelTitle.toLowerCase().includes('university')) { score += 0.1; } // Comments indicate engagement const commentCount = parseInt(video.commentCount || '0'); if (commentCount > 50) score += 0.05; return Math.min(Math.max(score, 0), 1); // Clamp between 0 and 1 } /** * Assess difficulty level of content */ private assessDifficultyLevel(video: YouTubeVideo, targetLevel: string): string { const title = video.title.toLowerCase(); const description = video.description.toLowerCase(); const content = `${title} ${description}`; const beginnerKeywords = ['beginner', 'basic', 'introduction', 'getting started', 'fundamentals', 'basics']; const intermediateKeywords = ['intermediate', 'advanced tutorial', 'in-depth', 'comprehensive']; const advancedKeywords = ['advanced', 'expert', 'professional', 'mastery', 'complex']; const beginnerScore = beginnerKeywords.filter(k => content.includes(k)).length; const intermediateScore = intermediateKeywords.filter(k => content.includes(k)).length; const advancedScore = advancedKeywords.filter(k => content.includes(k)).length; if (advancedScore > beginnerScore && advancedScore > intermediateScore) { return 'advanced'; } else if (intermediateScore > beginnerScore) { return 'intermediate'; } else if (beginnerScore > 0) { return 'beginner'; } // Default to target level if unclear return targetLevel; } /** * Generate learning sequence using LLM analysis */ private async generateLearningSequence( videos: Array<YouTubeVideo & { educationalScore: number; difficultyLevel: string }>, params: GenerateLearningPathParams ): Promise<LearningPath> { const prompt = this.buildLearningPathPrompt(videos, params); try { const response = await this.llmService.generateWithFallback({ prompt, model: 'gpt-4o-mini', // Cost-effective for structured tasks maxTokens: 2000, temperature: 0.1, responseFormat: 'json' }); const parsedResponse = JSON.parse(response.content); return this.structureLearningPath(parsedResponse, videos, params); } catch (error) { this.logger.warn('LLM generation failed, falling back to rule-based sequencing:', error); return this.generateRuleBasedSequence(videos, params); } } /** * Build prompt for LLM learning path generation */ private buildLearningPathPrompt( videos: Array<YouTubeVideo & { educationalScore: number; difficultyLevel: string }>, params: GenerateLearningPathParams ): string { const videoDescriptions = videos.map((video, index) => `${index + 1}. "${video.title}" (${video.difficultyLevel}) - ${video.duration} - ${video.description.slice(0, 100)}...` ).join('\n'); return `You are an expert educational content curator. Create a structured learning path for "${params.query}" at ${params.targetLevel} level. Available videos: ${videoDescriptions} Requirements: - Target level: ${params.targetLevel} - Maximum videos: ${params.maxVideos} - ${params.duration ? `Total duration target: ${params.duration}` : 'No duration constraint'} - ${params.includeQuizzes ? 'Include quiz questions for each video' : 'No quizzes needed'} Create a logical progression from basic concepts to more advanced topics. Consider: 1. Prerequisites and dependencies between topics 2. Optimal learning sequence 3. Difficulty progression 4. Learning objectives for each video 5. Key topics covered Respond with JSON in this format: { "topic": "${params.query}", "level": "${params.targetLevel}", "videos": [ { "order": 1, "videoIndex": 0, "prerequisites": ["concept1", "concept2"], "learningObjectives": ["objective1", "objective2"], "keyTopics": ["topic1", "topic2"], "estimatedStudyTime": "45 minutes" } ], "totalDuration": "estimated total time", "completionCriteria": ["criteria1", "criteria2"], "recommendations": ["tip1", "tip2"] }`; } /** * Structure the LLM response into a proper LearningPath */ private structureLearningPath( llmResponse: any, videos: Array<YouTubeVideo & { educationalScore: number; difficultyLevel: string }>, params: GenerateLearningPathParams ): LearningPath { const pathVideos = llmResponse.videos.map((videoData: any) => { const video = videos[videoData.videoIndex]; if (!video) { throw new Error(`Invalid video index: ${videoData.videoIndex}`); } return { videoId: video.id, title: video.title, order: videoData.order, difficulty: video.difficultyLevel, duration: video.duration || 'Unknown', prerequisites: videoData.prerequisites || [], learningObjectives: videoData.learningObjectives || [], keyTopics: videoData.keyTopics || [] }; }); return { topic: params.query, level: params.targetLevel, videos: pathVideos, totalDuration: llmResponse.totalDuration || 'Unknown', completionCriteria: llmResponse.completionCriteria || [], recommendations: llmResponse.recommendations || [] }; } /** * Fallback rule-based sequence generation */ private generateRuleBasedSequence( videos: Array<YouTubeVideo & { educationalScore: number; difficultyLevel: string }>, params: GenerateLearningPathParams ): LearningPath { // Sort videos by difficulty and educational score const sortedVideos = videos.sort((a, b) => { const difficultyOrder = { beginner: 1, intermediate: 2, advanced: 3 }; const aDiff = difficultyOrder[a.difficultyLevel] || 2; const bDiff = difficultyOrder[b.difficultyLevel] || 2; if (aDiff !== bDiff) { return aDiff - bDiff; // Sort by difficulty first } return b.educationalScore - a.educationalScore; // Then by quality }); const pathVideos = sortedVideos.slice(0, params.maxVideos).map((video, index) => ({ videoId: video.id, title: video.title, order: index + 1, difficulty: video.difficultyLevel, duration: video.duration || 'Unknown', prerequisites: index > 0 ? [`Video ${index}`] : [], learningObjectives: [`Learn ${params.query} concepts from this video`], keyTopics: [params.query] })); return { topic: params.query, level: params.targetLevel, videos: pathVideos, totalDuration: this.calculateTotalDuration(pathVideos), completionCriteria: [ 'Complete all videos in sequence', 'Take notes on key concepts', 'Practice examples where applicable' ], recommendations: [ 'Follow the suggested order for optimal learning', 'Take breaks between videos to process information', 'Revisit earlier videos if concepts become unclear' ] }; } /** * Enhance learning path with additional features */ private async enhanceLearningPath( path: LearningPath, params: GenerateLearningPathParams ): Promise<LearningPath> { if (!params.includeQuizzes) { return path; } // Generate quiz questions for each video using LLM try { const enhancedVideos = await Promise.all( path.videos.map(async (video) => { try { const quizPrompt = `Generate 3 multiple choice quiz questions for a video titled "${video.title}" about ${path.topic}. Make questions at ${path.level} level covering these topics: ${video.keyTopics.join(', ')}. Respond with JSON: { "questions": [ { "question": "question text", "options": ["A", "B", "C", "D"], "correctAnswer": 0, "explanation": "explanation" } ] }`; const response = await this.llmService.generateWithFallback({ prompt: quizPrompt, model: 'gpt-4o-mini', maxTokens: 1000, temperature: 0.2, responseFormat: 'json' }); const quizData = JSON.parse(response.content); return { ...video, quizQuestions: quizData.questions }; } catch (error) { this.logger.warn(`Failed to generate quiz for video ${video.videoId}:`, error); return video; // Return without quiz } }) ); return { ...path, videos: enhancedVideos }; } catch (error) { this.logger.warn('Failed to enhance learning path with quizzes:', error); return path; } } // Helper methods private removeDuplicates(videos: YouTubeVideo[]): YouTubeVideo[] { const seen = new Set(); return videos.filter(video => { if (seen.has(video.id)) { return false; } seen.add(video.id); return true; }); } private parseDuration(duration: string): number { const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/); if (!match) return 0; const hours = parseInt(match[1] || '0'); const minutes = parseInt(match[2] || '0'); const seconds = parseInt(match[3] || '0'); return hours * 3600 + minutes * 60 + seconds; } private calculateTotalDuration(videos: Array<{ duration: string }>): string { const totalSeconds = videos.reduce((sum, video) => { return sum + this.parseDuration(video.duration || 'PT0S'); }, 0); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); if (hours > 0) { return `${hours}h ${minutes}m`; } return `${minutes}m`; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/efikuta/youtube-knowledge-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server