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.
- ambivo_agents/__init__.py +91 -0
- ambivo_agents/agents/__init__.py +21 -0
- ambivo_agents/agents/assistant.py +203 -0
- ambivo_agents/agents/code_executor.py +133 -0
- ambivo_agents/agents/code_executor2.py +222 -0
- ambivo_agents/agents/knowledge_base.py +935 -0
- ambivo_agents/agents/media_editor.py +992 -0
- ambivo_agents/agents/moderator.py +617 -0
- ambivo_agents/agents/simple_web_search.py +404 -0
- ambivo_agents/agents/web_scraper.py +1027 -0
- ambivo_agents/agents/web_search.py +933 -0
- ambivo_agents/agents/youtube_download.py +784 -0
- ambivo_agents/cli.py +699 -0
- ambivo_agents/config/__init__.py +4 -0
- ambivo_agents/config/loader.py +301 -0
- ambivo_agents/core/__init__.py +33 -0
- ambivo_agents/core/base.py +1024 -0
- ambivo_agents/core/history.py +606 -0
- ambivo_agents/core/llm.py +333 -0
- ambivo_agents/core/memory.py +640 -0
- ambivo_agents/executors/__init__.py +8 -0
- ambivo_agents/executors/docker_executor.py +108 -0
- ambivo_agents/executors/media_executor.py +237 -0
- ambivo_agents/executors/youtube_executor.py +404 -0
- ambivo_agents/services/__init__.py +6 -0
- ambivo_agents/services/agent_service.py +605 -0
- ambivo_agents/services/factory.py +370 -0
- ambivo_agents-1.0.1.dist-info/METADATA +1090 -0
- ambivo_agents-1.0.1.dist-info/RECORD +33 -0
- ambivo_agents-1.0.1.dist-info/WHEEL +5 -0
- ambivo_agents-1.0.1.dist-info/entry_points.txt +3 -0
- ambivo_agents-1.0.1.dist-info/licenses/LICENSE +21 -0
- ambivo_agents-1.0.1.dist-info/top_level.txt +1 -0
@@ -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
|
+
)
|