ambivo-agents 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,784 @@
1
+ # ambivo_agents/agents/youtube_download.py
2
+ """
3
+ YouTube Download Agent with pytubefix integration
4
+ Handles YouTube video and audio downloads using Docker containers
5
+ Updated with LLM-aware intent detection and conversation history integration.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import uuid
11
+ import time
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Dict, List, Any, Optional
15
+ from datetime import datetime
16
+ import logging
17
+
18
+ from ..core.base import BaseAgent, AgentRole, AgentMessage, MessageType, ExecutionContext, AgentTool
19
+ from ..config.loader import load_config, get_config_section
20
+ from ..core.history import WebAgentHistoryMixin, ContextType
21
+ from ..executors.youtube_executor import YouTubeDockerExecutor
22
+
23
+
24
+ class YouTubeDownloadAgent(BaseAgent, WebAgentHistoryMixin):
25
+ """YouTube Download Agent for downloading videos and audio from YouTube"""
26
+
27
+ def __init__(self, agent_id: str = None, memory_manager=None, llm_service=None, **kwargs):
28
+ if agent_id is None:
29
+ agent_id = f"youtube_{str(uuid.uuid4())[:8]}"
30
+
31
+ super().__init__(
32
+ agent_id=agent_id,
33
+ role=AgentRole.CODE_EXECUTOR,
34
+ memory_manager=memory_manager,
35
+ llm_service=llm_service,
36
+ name="YouTube Download Agent",
37
+ description="Agent for downloading videos and audio from YouTube using pytubefix",
38
+ **kwargs
39
+ )
40
+
41
+ # Initialize history mixin
42
+ self.setup_history_mixin()
43
+
44
+ # Load YouTube configuration
45
+ try:
46
+ if hasattr(self, 'config') and self.config:
47
+ self.youtube_config = self.config.get('youtube_download', {})
48
+ else:
49
+ config = load_config()
50
+ self.youtube_config = config.get('youtube_download', {})
51
+ except Exception as e:
52
+ # Provide sensible defaults if config fails
53
+ self.youtube_config = {
54
+ 'docker_image': 'sgosain/amb-ubuntu-python-public-pod',
55
+ 'download_dir': './youtube_downloads',
56
+ 'timeout': 600,
57
+ 'memory_limit': '1g',
58
+ 'default_audio_only': True
59
+ }
60
+
61
+ # YouTube-specific initialization
62
+ self._load_youtube_config()
63
+ self._initialize_youtube_executor()
64
+ self._add_youtube_tools()
65
+
66
+ async def _llm_analyze_youtube_intent(self, user_message: str, conversation_context: str = "") -> Dict[str, Any]:
67
+ """Use LLM to analyze YouTube download intent"""
68
+ if not self.llm_service:
69
+ return self._keyword_based_youtube_analysis(user_message)
70
+
71
+ prompt = f"""
72
+ Analyze this user message in the context of YouTube operations and extract:
73
+ 1. Primary intent (download_audio, download_video, get_info, batch_download, help_request)
74
+ 2. YouTube URLs mentioned
75
+ 3. Output preferences (audio/video, format, quality)
76
+ 4. Context references (referring to previous YouTube operations)
77
+ 5. Custom filename preferences
78
+
79
+ Conversation Context:
80
+ {conversation_context}
81
+
82
+ Current User Message: {user_message}
83
+
84
+ Respond in JSON format:
85
+ {{
86
+ "primary_intent": "download_audio|download_video|get_info|batch_download|help_request",
87
+ "youtube_urls": ["https://youtube.com/watch?v=example"],
88
+ "output_preferences": {{
89
+ "format_type": "audio|video",
90
+ "format": "mp3|mp4|wav",
91
+ "quality": "high|medium|low",
92
+ "custom_filename": "filename or null"
93
+ }},
94
+ "uses_context_reference": true/false,
95
+ "context_type": "previous_url|previous_download",
96
+ "confidence": 0.0-1.0
97
+ }}
98
+ """
99
+
100
+ try:
101
+ response = await self.llm_service.generate_response(prompt)
102
+ import re
103
+ json_match = re.search(r'\{.*\}', response, re.DOTALL)
104
+ if json_match:
105
+ return json.loads(json_match.group())
106
+ else:
107
+ return self._extract_youtube_intent_from_llm_response(response, user_message)
108
+ except Exception as e:
109
+ return self._keyword_based_youtube_analysis(user_message)
110
+
111
+ def _keyword_based_youtube_analysis(self, user_message: str) -> Dict[str, Any]:
112
+ """Fallback keyword-based YouTube intent analysis"""
113
+ content_lower = user_message.lower()
114
+
115
+ # Determine intent
116
+ if any(word in content_lower for word in ['info', 'information', 'details', 'about']):
117
+ intent = 'get_info'
118
+ elif any(word in content_lower for word in ['audio', 'mp3', 'music', 'sound']):
119
+ intent = 'download_audio'
120
+ elif any(word in content_lower for word in ['video', 'mp4', 'watch']):
121
+ intent = 'download_video'
122
+ elif any(word in content_lower for word in ['batch', 'multiple', 'several']):
123
+ intent = 'batch_download'
124
+ elif any(word in content_lower for word in ['download', 'get', 'fetch']):
125
+ # Default to audio if no specific format mentioned
126
+ intent = 'download_audio'
127
+ else:
128
+ intent = 'help_request'
129
+
130
+ # Extract YouTube URLs
131
+ youtube_urls = self._extract_youtube_urls(user_message)
132
+
133
+ # Determine output preferences
134
+ format_type = "video" if intent == 'download_video' else "audio"
135
+ output_format = None
136
+ if 'mp3' in content_lower:
137
+ output_format = 'mp3'
138
+ elif 'mp4' in content_lower:
139
+ output_format = 'mp4'
140
+ elif 'wav' in content_lower:
141
+ output_format = 'wav'
142
+
143
+ quality = 'medium'
144
+ if 'high' in content_lower:
145
+ quality = 'high'
146
+ elif 'low' in content_lower:
147
+ quality = 'low'
148
+
149
+ return {
150
+ "primary_intent": intent,
151
+ "youtube_urls": youtube_urls,
152
+ "output_preferences": {
153
+ "format_type": format_type,
154
+ "format": output_format,
155
+ "quality": quality,
156
+ "custom_filename": None
157
+ },
158
+ "uses_context_reference": any(word in content_lower for word in ['this', 'that', 'it']),
159
+ "context_type": "previous_url",
160
+ "confidence": 0.7
161
+ }
162
+
163
+ async def process_message(self, message: AgentMessage, context: ExecutionContext = None) -> AgentMessage:
164
+ """Process message with LLM-based YouTube intent detection and history context"""
165
+ self.memory.store_message(message)
166
+
167
+ try:
168
+ user_message = message.content
169
+
170
+ # Update conversation state
171
+ self.update_conversation_state(user_message)
172
+
173
+ # Get conversation context for LLM analysis
174
+ conversation_context = self._get_youtube_conversation_context_summary()
175
+
176
+ # Use LLM to analyze intent
177
+ intent_analysis = await self._llm_analyze_youtube_intent(user_message, conversation_context)
178
+
179
+ # Route request based on LLM analysis
180
+ response_content = await self._route_youtube_with_llm_analysis(intent_analysis, user_message, context)
181
+
182
+ response = self.create_response(
183
+ content=response_content,
184
+ recipient_id=message.sender_id,
185
+ session_id=message.session_id,
186
+ conversation_id=message.conversation_id
187
+ )
188
+
189
+ self.memory.store_message(response)
190
+ return response
191
+
192
+ except Exception as e:
193
+ error_response = self.create_response(
194
+ content=f"YouTube Download Agent error: {str(e)}",
195
+ recipient_id=message.sender_id,
196
+ message_type=MessageType.ERROR,
197
+ session_id=message.session_id,
198
+ conversation_id=message.conversation_id
199
+ )
200
+ return error_response
201
+
202
+ def _get_youtube_conversation_context_summary(self) -> str:
203
+ """Get YouTube conversation context summary"""
204
+ try:
205
+ recent_history = self.get_conversation_history_with_context(
206
+ limit=3,
207
+ context_types=[ContextType.URL]
208
+ )
209
+
210
+ context_summary = []
211
+ for msg in recent_history:
212
+ if msg.get('message_type') == 'user_input':
213
+ extracted_context = msg.get('extracted_context', {})
214
+ urls = extracted_context.get('url', [])
215
+
216
+ youtube_urls = [url for url in urls if self._is_valid_youtube_url(url)]
217
+ if youtube_urls:
218
+ context_summary.append(f"Previous YouTube URL: {youtube_urls[0]}")
219
+
220
+ return "\n".join(context_summary) if context_summary else "No previous YouTube context"
221
+ except:
222
+ return "No previous YouTube context"
223
+
224
+ async def _route_youtube_with_llm_analysis(self, intent_analysis: Dict[str, Any], user_message: str,
225
+ context: ExecutionContext) -> str:
226
+ """Route YouTube request based on LLM intent analysis"""
227
+
228
+ primary_intent = intent_analysis.get("primary_intent", "help_request")
229
+ youtube_urls = intent_analysis.get("youtube_urls", [])
230
+ output_prefs = intent_analysis.get("output_preferences", {})
231
+ uses_context = intent_analysis.get("uses_context_reference", False)
232
+
233
+ # Resolve context references if needed
234
+ if uses_context and not youtube_urls:
235
+ recent_url = self._get_recent_youtube_url_from_history(context)
236
+ if recent_url:
237
+ youtube_urls = [recent_url]
238
+
239
+ # Route based on intent
240
+ if primary_intent == "help_request":
241
+ return await self._handle_youtube_help_request(user_message)
242
+ elif primary_intent == "download_audio":
243
+ return await self._handle_audio_download(youtube_urls, output_prefs, user_message)
244
+ elif primary_intent == "download_video":
245
+ return await self._handle_video_download(youtube_urls, output_prefs, user_message)
246
+ elif primary_intent == "get_info":
247
+ return await self._handle_video_info(youtube_urls, user_message)
248
+ elif primary_intent == "batch_download":
249
+ return await self._handle_batch_download(youtube_urls, output_prefs, user_message)
250
+ else:
251
+ return await self._handle_youtube_help_request(user_message)
252
+
253
+ async def _handle_audio_download(self, youtube_urls: List[str], output_prefs: Dict[str, Any], user_message: str) -> str:
254
+ """Handle audio download requests"""
255
+ if not youtube_urls:
256
+ recent_url = self._get_recent_youtube_url_from_history(None)
257
+ if recent_url:
258
+ return f"I can download audio from YouTube videos. Did you mean to download audio from **{recent_url}**? Please confirm."
259
+ else:
260
+ return "I can download audio from YouTube videos. Please provide a YouTube URL.\n\n" \
261
+ "Example: 'Download audio from https://youtube.com/watch?v=example'"
262
+
263
+ url = youtube_urls[0]
264
+ output_format = output_prefs.get("format", "mp3")
265
+ quality = output_prefs.get("quality", "medium")
266
+ custom_filename = output_prefs.get("custom_filename")
267
+
268
+ try:
269
+ result = await self._download_youtube_audio(url, custom_filename)
270
+
271
+ if result['success']:
272
+ file_size_mb = result.get('file_size_bytes', 0) / (1024 * 1024)
273
+ return f"""✅ **YouTube Audio Download Completed**
274
+
275
+ 🎵 **Type:** Audio (MP3)
276
+ 🔗 **URL:** {url}
277
+ 📁 **File:** {result.get('filename', 'Unknown')}
278
+ 📍 **Location:** {result.get('file_path', 'Unknown')}
279
+ 📊 **Size:** {file_size_mb:.2f} MB
280
+ ⏱️ **Download Time:** {result.get('execution_time', 0):.2f}s
281
+
282
+ Your audio file has been successfully downloaded and is ready to use! 🎉"""
283
+ else:
284
+ return f"❌ **Audio download failed:** {result['error']}"
285
+
286
+ except Exception as e:
287
+ return f"❌ **Error during audio download:** {str(e)}"
288
+
289
+ async def _handle_video_download(self, youtube_urls: List[str], output_prefs: Dict[str, Any], user_message: str) -> str:
290
+ """Handle video download requests"""
291
+ if not youtube_urls:
292
+ recent_url = self._get_recent_youtube_url_from_history(None)
293
+ if recent_url:
294
+ return f"I can download videos from YouTube. Did you mean to download the video from **{recent_url}**? Please confirm."
295
+ else:
296
+ return "I can download videos from YouTube. Please provide a YouTube URL.\n\n" \
297
+ "Example: 'Download video from https://youtube.com/watch?v=example'"
298
+
299
+ url = youtube_urls[0]
300
+ custom_filename = output_prefs.get("custom_filename")
301
+
302
+ try:
303
+ result = await self._download_youtube_video(url, custom_filename)
304
+
305
+ if result['success']:
306
+ file_size_mb = result.get('file_size_bytes', 0) / (1024 * 1024)
307
+ return f"""✅ **YouTube Video Download Completed**
308
+
309
+ 🎬 **Type:** Video (MP4)
310
+ 🔗 **URL:** {url}
311
+ 📁 **File:** {result.get('filename', 'Unknown')}
312
+ 📍 **Location:** {result.get('file_path', 'Unknown')}
313
+ 📊 **Size:** {file_size_mb:.2f} MB
314
+ ⏱️ **Download Time:** {result.get('execution_time', 0):.2f}s
315
+
316
+ Your video file has been successfully downloaded and is ready to use! 🎉"""
317
+ else:
318
+ return f"❌ **Video download failed:** {result['error']}"
319
+
320
+ except Exception as e:
321
+ return f"❌ **Error during video download:** {str(e)}"
322
+
323
+ async def _handle_video_info(self, youtube_urls: List[str], user_message: str) -> str:
324
+ """Handle video info requests"""
325
+ if not youtube_urls:
326
+ recent_url = self._get_recent_youtube_url_from_history(None)
327
+ if recent_url:
328
+ return f"I can get information about YouTube videos. Did you mean to get info for **{recent_url}**? Please confirm."
329
+ else:
330
+ return "I can get information about YouTube videos. Please provide a YouTube URL.\n\n" \
331
+ "Example: 'Get info about https://youtube.com/watch?v=example'"
332
+
333
+ url = youtube_urls[0]
334
+
335
+ try:
336
+ result = await self._get_youtube_info(url)
337
+
338
+ if result['success']:
339
+ video_info = result['video_info']
340
+ return f"""📹 **YouTube Video Information**
341
+
342
+ **🎬 Title:** {video_info.get('title', 'Unknown')}
343
+ **👤 Author:** {video_info.get('author', 'Unknown')}
344
+ **⏱️ Duration:** {self._format_duration(video_info.get('duration', 0))}
345
+ **👀 Views:** {video_info.get('views', 0):,}
346
+ **🔗 URL:** {url}
347
+
348
+ **📊 Available Streams:**
349
+ - Audio streams: {video_info.get('available_streams', {}).get('audio_streams', 0)}
350
+ - Video streams: {video_info.get('available_streams', {}).get('video_streams', 0)}
351
+ - Highest resolution: {video_info.get('available_streams', {}).get('highest_resolution', 'Unknown')}
352
+
353
+ Would you like me to download this video?"""
354
+ else:
355
+ return f"❌ **Error getting video info:** {result['error']}"
356
+
357
+ except Exception as e:
358
+ return f"❌ **Error getting video info:** {str(e)}"
359
+
360
+ async def _handle_batch_download(self, youtube_urls: List[str], output_prefs: Dict[str, Any], user_message: str) -> str:
361
+ """Handle batch download requests"""
362
+ if not youtube_urls:
363
+ return "I can download multiple YouTube videos at once. Please provide YouTube URLs.\n\n" \
364
+ "Example: 'Download https://youtube.com/watch?v=1 and https://youtube.com/watch?v=2'"
365
+
366
+ format_type = output_prefs.get("format_type", "audio")
367
+ audio_only = format_type == "audio"
368
+
369
+ try:
370
+ result = await self._batch_download_youtube(youtube_urls, audio_only)
371
+
372
+ if result['success']:
373
+ successful = result['successful']
374
+ failed = result['failed']
375
+ total = result['total_urls']
376
+
377
+ response = f"""📦 **Batch YouTube Download Completed**
378
+
379
+ 📊 **Summary:**
380
+ - **Total URLs:** {total}
381
+ - **Successful:** {successful}
382
+ - **Failed:** {failed}
383
+ - **Type:** {'Audio' if audio_only else 'Video'}
384
+
385
+ """
386
+
387
+ if successful > 0:
388
+ response += "✅ **Successfully Downloaded:**\n"
389
+ for i, download_result in enumerate(result['results'], 1):
390
+ if download_result.get('success', False):
391
+ response += f"{i}. {download_result.get('filename', 'Unknown')}\n"
392
+
393
+ if failed > 0:
394
+ response += f"\n❌ **Failed Downloads:** {failed}\n"
395
+ for i, download_result in enumerate(result['results'], 1):
396
+ if not download_result.get('success', False):
397
+ response += f"{i}. {download_result.get('url', 'Unknown')}: {download_result.get('error', 'Unknown error')}\n"
398
+
399
+ response += f"\n🎉 Batch download completed with {successful}/{total} successful downloads!"
400
+ return response
401
+ else:
402
+ return f"❌ **Batch download failed:** {result['error']}"
403
+
404
+ except Exception as e:
405
+ return f"❌ **Error during batch download:** {str(e)}"
406
+
407
+ async def _handle_youtube_help_request(self, user_message: str) -> str:
408
+ """Handle YouTube help requests with conversation context"""
409
+ state = self.get_conversation_state()
410
+
411
+ response = ("I'm your YouTube Download Agent! I can help you with:\n\n"
412
+ "🎵 **Audio Downloads**\n"
413
+ "- Download MP3 audio from YouTube videos\n"
414
+ "- High-quality audio extraction\n"
415
+ "- Custom filename support\n\n"
416
+ "🎥 **Video Downloads**\n"
417
+ "- Download MP4 videos in highest available quality\n"
418
+ "- Progressive download format\n"
419
+ "- Full video with audio\n\n"
420
+ "📊 **Video Information**\n"
421
+ "- Get video details without downloading\n"
422
+ "- Check duration, views, and available streams\n"
423
+ "- Thumbnail and metadata extraction\n\n"
424
+ "📦 **Batch Operations**\n"
425
+ "- Download multiple videos at once\n"
426
+ "- Bulk audio/video processing\n\n"
427
+ "🧠 **Smart Context Features**\n"
428
+ "- Remembers YouTube URLs from conversation\n"
429
+ "- Understands 'that video' and 'this URL'\n"
430
+ "- Maintains working context\n\n")
431
+
432
+ # Add current context information
433
+ if state.current_resource and self._is_valid_youtube_url(state.current_resource):
434
+ response += f"🎯 **Current Video:** {state.current_resource}\n"
435
+
436
+ response += "\n💡 **Examples:**\n"
437
+ response += "• 'Download audio from https://youtube.com/watch?v=example'\n"
438
+ response += "• 'Download video from https://youtube.com/watch?v=example'\n"
439
+ response += "• 'Get info about https://youtube.com/watch?v=example'\n"
440
+ response += "• 'Download that video as audio'\n"
441
+ response += "\nI understand context from our conversation! 🚀"
442
+
443
+ return response
444
+
445
+ def _extract_youtube_intent_from_llm_response(self, llm_response: str, user_message: str) -> Dict[str, Any]:
446
+ """Extract YouTube intent from non-JSON LLM response"""
447
+ content_lower = llm_response.lower()
448
+
449
+ if 'audio' in content_lower or 'mp3' in content_lower:
450
+ intent = 'download_audio'
451
+ elif 'video' in content_lower or 'mp4' in content_lower:
452
+ intent = 'download_video'
453
+ elif 'info' in content_lower or 'information' in content_lower:
454
+ intent = 'get_info'
455
+ elif 'batch' in content_lower or 'multiple' in content_lower:
456
+ intent = 'batch_download'
457
+ else:
458
+ intent = 'help_request'
459
+
460
+ return {
461
+ "primary_intent": intent,
462
+ "youtube_urls": [],
463
+ "output_preferences": {"format_type": "audio", "format": None, "quality": "medium"},
464
+ "uses_context_reference": False,
465
+ "context_type": "none",
466
+ "confidence": 0.6
467
+ }
468
+
469
+ def _add_youtube_tools(self):
470
+ """Add all YouTube download tools"""
471
+
472
+ # Download video/audio tool
473
+ self.add_tool(AgentTool(
474
+ name="download_youtube",
475
+ description="Download video or audio from YouTube URL",
476
+ function=self._download_youtube,
477
+ parameters_schema={
478
+ "type": "object",
479
+ "properties": {
480
+ "url": {"type": "string", "description": "YouTube URL to download"},
481
+ "audio_only": {"type": "boolean", "default": True, "description": "Download only audio if True"},
482
+ "custom_filename": {"type": "string", "description": "Custom filename (optional)"}
483
+ },
484
+ "required": ["url"]
485
+ }
486
+ ))
487
+
488
+ # Get video information tool
489
+ self.add_tool(AgentTool(
490
+ name="get_youtube_info",
491
+ description="Get information about a YouTube video without downloading",
492
+ function=self._get_youtube_info,
493
+ parameters_schema={
494
+ "type": "object",
495
+ "properties": {
496
+ "url": {"type": "string", "description": "YouTube URL to get information about"}
497
+ },
498
+ "required": ["url"]
499
+ }
500
+ ))
501
+
502
+ # Download audio specifically
503
+ self.add_tool(AgentTool(
504
+ name="download_youtube_audio",
505
+ description="Download audio only from YouTube URL",
506
+ function=self._download_youtube_audio,
507
+ parameters_schema={
508
+ "type": "object",
509
+ "properties": {
510
+ "url": {"type": "string", "description": "YouTube URL to download audio from"},
511
+ "custom_filename": {"type": "string", "description": "Custom filename (optional)"}
512
+ },
513
+ "required": ["url"]
514
+ }
515
+ ))
516
+
517
+ # Download video specifically
518
+ self.add_tool(AgentTool(
519
+ name="download_youtube_video",
520
+ description="Download video from YouTube URL",
521
+ function=self._download_youtube_video,
522
+ parameters_schema={
523
+ "type": "object",
524
+ "properties": {
525
+ "url": {"type": "string", "description": "YouTube URL to download video from"},
526
+ "custom_filename": {"type": "string", "description": "Custom filename (optional)"}
527
+ },
528
+ "required": ["url"]
529
+ }
530
+ ))
531
+
532
+ # Batch download tool
533
+ self.add_tool(AgentTool(
534
+ name="batch_download_youtube",
535
+ description="Download multiple YouTube videos/audio",
536
+ function=self._batch_download_youtube,
537
+ parameters_schema={
538
+ "type": "object",
539
+ "properties": {
540
+ "urls": {"type": "array", "items": {"type": "string"}, "description": "List of YouTube URLs"},
541
+ "audio_only": {"type": "boolean", "default": True, "description": "Download only audio if True"}
542
+ },
543
+ "required": ["urls"]
544
+ }
545
+ ))
546
+
547
+ def _load_youtube_config(self):
548
+ """Load YouTube configuration with fallbacks"""
549
+ try:
550
+ if hasattr(self, 'config') and self.config:
551
+ self.youtube_config = self.config.get('youtube_download', {})
552
+ logging.info("Loaded YouTube config from agent config")
553
+ else:
554
+ config = load_config()
555
+ self.youtube_config = config.get('youtube_download', {})
556
+ logging.info("Loaded YouTube config from file")
557
+ except Exception as e:
558
+ # Provide sensible defaults if config fails
559
+ self.youtube_config = {
560
+ 'docker_image': 'sgosain/amb-ubuntu-python-public-pod',
561
+ 'download_dir': './youtube_downloads',
562
+ 'timeout': 600,
563
+ 'memory_limit': '1g',
564
+ 'default_audio_only': True
565
+ }
566
+
567
+ def _initialize_youtube_executor(self):
568
+ """Initialize the YouTube executor"""
569
+ try:
570
+ from ..executors.youtube_executor import YouTubeDockerExecutor
571
+ self.youtube_executor = YouTubeDockerExecutor(self.youtube_config)
572
+ logging.info("YouTube executor initialized successfully")
573
+ except Exception as e:
574
+ logging.error(f"Failed to initialize YouTube executor: {e}")
575
+ raise RuntimeError(f"Failed to initialize YouTube executor: {e}")
576
+
577
+ async def _download_youtube(self, url: str, audio_only: bool = True, custom_filename: str = None) -> Dict[str, Any]:
578
+ """Download video or audio from YouTube"""
579
+ try:
580
+ if not self._is_valid_youtube_url(url):
581
+ return {"success": False, "error": f"Invalid YouTube URL: {url}"}
582
+
583
+ result = self.youtube_executor.download_youtube_video(
584
+ url=url,
585
+ audio_only=audio_only,
586
+ output_filename=custom_filename
587
+ )
588
+
589
+ if result['success']:
590
+ download_info = result.get('download_info', {})
591
+ return {
592
+ "success": True,
593
+ "message": f"Successfully downloaded {'audio' if audio_only else 'video'} from YouTube",
594
+ "url": url,
595
+ "audio_only": audio_only,
596
+ "file_path": download_info.get('final_path'),
597
+ "filename": download_info.get('filename'),
598
+ "file_size_bytes": download_info.get('size_bytes', 0),
599
+ "execution_time": result['execution_time'],
600
+ "custom_filename": custom_filename
601
+ }
602
+ else:
603
+ return result
604
+
605
+ except Exception as e:
606
+ return {"success": False, "error": str(e)}
607
+
608
+ async def _download_youtube_audio(self, url: str, custom_filename: str = None) -> Dict[str, Any]:
609
+ """Download audio only from YouTube"""
610
+ return await self._download_youtube(url, audio_only=True, custom_filename=custom_filename)
611
+
612
+ async def _download_youtube_video(self, url: str, custom_filename: str = None) -> Dict[str, Any]:
613
+ """Download video from YouTube"""
614
+ return await self._download_youtube(url, audio_only=False, custom_filename=custom_filename)
615
+
616
+ async def _get_youtube_info(self, url: str) -> Dict[str, Any]:
617
+ """Get YouTube video information"""
618
+ try:
619
+ if not self._is_valid_youtube_url(url):
620
+ return {"success": False, "error": f"Invalid YouTube URL: {url}"}
621
+
622
+ result = self.youtube_executor.get_video_info(url)
623
+
624
+ if result['success']:
625
+ return {
626
+ "success": True,
627
+ "message": "Successfully retrieved video information",
628
+ "url": url,
629
+ "video_info": result['video_info']
630
+ }
631
+ else:
632
+ return result
633
+
634
+ except Exception as e:
635
+ return {"success": False, "error": str(e)}
636
+
637
+ async def _batch_download_youtube(self, urls: List[str], audio_only: bool = True) -> Dict[str, Any]:
638
+ """Download multiple YouTube videos/audio"""
639
+ try:
640
+ results = []
641
+ successful = 0
642
+ failed = 0
643
+
644
+ for i, url in enumerate(urls):
645
+ try:
646
+ result = await self._download_youtube(url, audio_only=audio_only)
647
+ results.append(result)
648
+
649
+ if result.get('success', False):
650
+ successful += 1
651
+ else:
652
+ failed += 1
653
+
654
+ # Add delay between downloads to be respectful
655
+ if i < len(urls) - 1:
656
+ await asyncio.sleep(2)
657
+
658
+ except Exception as e:
659
+ results.append({
660
+ "success": False,
661
+ "url": url,
662
+ "error": str(e)
663
+ })
664
+ failed += 1
665
+
666
+ return {
667
+ "success": True,
668
+ "message": f"Batch download completed: {successful} successful, {failed} failed",
669
+ "total_urls": len(urls),
670
+ "successful": successful,
671
+ "failed": failed,
672
+ "audio_only": audio_only,
673
+ "results": results
674
+ }
675
+
676
+ except Exception as e:
677
+ return {"success": False, "error": str(e)}
678
+
679
+ def _is_valid_youtube_url(self, url: str) -> bool:
680
+ """Check if URL is a valid YouTube URL"""
681
+ youtube_patterns = [
682
+ r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/',
683
+ r'(https?://)?(www\.)?youtu\.be/',
684
+ r'(https?://)?(www\.)?youtube\.com/watch\?v=',
685
+ r'(https?://)?(www\.)?youtube\.com/embed/',
686
+ r'(https?://)?(www\.)?youtube\.com/v/',
687
+ ]
688
+
689
+ return any(re.match(pattern, url, re.IGNORECASE) for pattern in youtube_patterns)
690
+
691
+ def _extract_youtube_urls(self, text: str) -> List[str]:
692
+ """Extract YouTube URLs from text"""
693
+ youtube_patterns = [
694
+ r'https?://(?:www\.)?youtube\.com/watch\?v=[\w-]+',
695
+ r'https?://(?:www\.)?youtu\.be/[\w-]+',
696
+ r'https?://(?:www\.)?youtube\.com/embed/[\w-]+',
697
+ r'https?://(?:www\.)?youtube\.com/v/[\w-]+',
698
+ ]
699
+
700
+ urls = []
701
+ for pattern in youtube_patterns:
702
+ urls.extend(re.findall(pattern, text, re.IGNORECASE))
703
+
704
+ return list(set(urls)) # Remove duplicates
705
+
706
+ def _get_recent_youtube_url_from_history(self, context):
707
+ """Get most recent YouTube URL from conversation history"""
708
+ try:
709
+ history = self.memory.get_recent_messages(limit=5, conversation_id=context.conversation_id if context else None)
710
+ for msg in reversed(history):
711
+ if isinstance(msg, dict):
712
+ content = msg.get('content', '')
713
+ urls = self._extract_youtube_urls(content)
714
+ if urls:
715
+ return urls[0]
716
+ except:
717
+ pass
718
+ return None
719
+
720
+ def _format_duration(self, seconds: int) -> str:
721
+ """Format duration in seconds to readable format"""
722
+ if seconds < 60:
723
+ return f"{seconds}s"
724
+ elif seconds < 3600:
725
+ minutes = seconds // 60
726
+ remaining_seconds = seconds % 60
727
+ return f"{minutes}m {remaining_seconds}s"
728
+ else:
729
+ hours = seconds // 3600
730
+ remaining_minutes = (seconds % 3600) // 60
731
+ remaining_seconds = seconds % 60
732
+ return f"{hours}h {remaining_minutes}m {remaining_seconds}s"
733
+
734
+ @classmethod
735
+ def create_simple(cls, agent_id: str = None, **kwargs):
736
+ """
737
+ Create agent with auto-configuration (recommended for most users)
738
+
739
+ Args:
740
+ agent_id: Optional agent ID. If None, auto-generates one.
741
+ **kwargs: Additional arguments passed to constructor
742
+
743
+ Returns:
744
+ YouTubeDownloadAgent: Configured agent ready to use
745
+ """
746
+ # Auto-generate ID if not provided
747
+ if agent_id is None:
748
+ agent_id = f"youtube_{str(uuid.uuid4())[:8]}"
749
+
750
+ # Create with auto-configuration enabled
751
+ return cls(
752
+ agent_id=agent_id,
753
+ auto_configure=True, # Enable auto-configuration
754
+ **kwargs
755
+ )
756
+
757
+ @classmethod
758
+ def create_advanced(cls,
759
+ agent_id: str,
760
+ memory_manager,
761
+ llm_service=None,
762
+ config: Dict[str, Any] = None,
763
+ **kwargs):
764
+ """
765
+ Create agent with explicit dependencies (for advanced use cases)
766
+
767
+ Args:
768
+ agent_id: Agent identifier
769
+ memory_manager: Pre-configured memory manager
770
+ llm_service: Optional pre-configured LLM service
771
+ config: Optional configuration dictionary
772
+ **kwargs: Additional arguments passed to constructor
773
+
774
+ Returns:
775
+ YouTubeDownloadAgent: Agent with explicit dependencies
776
+ """
777
+ return cls(
778
+ agent_id=agent_id,
779
+ memory_manager=memory_manager,
780
+ llm_service=llm_service,
781
+ config=config,
782
+ auto_configure=False, # Disable auto-config when using advanced mode
783
+ **kwargs
784
+ )