generate_learning_path
Create structured learning paths from YouTube videos tailored to your skill level, with difficulty assessment and optional quiz generation for effective topic mastery.
Instructions
Generate AI-powered learning paths from YouTube content with difficulty assessment
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| includeQuizzes | No | Whether to generate quiz questions | |
| maxVideos | No | Maximum number of videos to include | |
| query | Yes | Topic or subject for the learning path | |
| targetLevel | No | Target skill level for the learning path | beginner |
Implementation Reference
- src/tools/learning-path.ts:22-61 (handler)The main handler method 'execute' that implements the core logic of the 'generate_learning_path' tool: parses input args, checks cache, searches videos, analyzes/ranks them, generates sequence with LLM, enhances with quizzes, and caches result.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; } }
- src/tools/learning-path.ts:13-491 (handler)Full LearningPathGenerator class implementing all aspects of learning path generation including video search, ranking, LLM orchestration, quiz generation, caching, and helpers.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`; } }
- src/types.ts:233-239 (schema)Zod schema defining input parameters for the generate_learning_path tool, used for validation in the handler.export const GenerateLearningPathSchema = z.object({ query: z.string().describe('Topic or subject for the learning path'), targetLevel: z.enum(['beginner', 'intermediate', 'advanced']).default('beginner').describe('Target difficulty level'), maxVideos: z.number().min(5).max(50).default(20).describe('Maximum number of videos in the path'), duration: z.string().optional().describe('Preferred total duration (e.g., "2 hours", "30 minutes")'), includeQuizzes: z.boolean().default(false).describe('Whether to generate quiz questions'), });
- src/index.ts:383-413 (registration)Tool registration in listTools handler, defining name, description, and inputSchema for generate_learning_path.name: 'generate_learning_path', description: 'Generate AI-powered learning paths from YouTube content with difficulty assessment', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Topic or subject for the learning path' }, targetLevel: { type: 'string', enum: ['beginner', 'intermediate', 'advanced'], default: 'beginner', description: 'Target skill level for the learning path' }, maxVideos: { type: 'number', minimum: 5, maximum: 50, default: 20, description: 'Maximum number of videos to include' }, includeQuizzes: { type: 'boolean', default: false, description: 'Whether to generate quiz questions' } }, required: ['query'] } },
- src/index.ts:580-582 (registration)Dispatch case in the main CallToolRequestSchema handler that routes calls to generate_learning_path to the LearningPathGenerator.execute method.case 'generate_learning_path': result = await this.learningPathTool.execute(args); break;