from smolagents import tool, Tool from youtube_transcript_api import YouTubeTranscriptApi from urllib.parse import urlparse, parse_qs import os import ast import operator as op from typing import Any, Optional from datetime import datetime from smolagents import tool @tool def youtube_tool(video_url: str) -> str: """ Analyzes a YouTube video by extracting its transcript. Args: video_url (str): The full URL of the YouTube video. Returns: str: The transcript text of the video or an error message. """ try: # Extract video ID from URL parsed_url = urlparse(video_url) if parsed_url.hostname == 'youtu.be': video_id = parsed_url.path[1:] else: video_id = parse_qs(parsed_url.query).get('v', [None])[0] if not video_id: return "Error: Could not extract video ID from URL." # Fetch transcript transcript_list = YouTubeTranscriptApi.fetch(video_id) # Combine transcript parts into a single string full_text = " ".join([item['text'] for item in transcript_list]) return full_text except Exception as e: return f"Error analyzing YouTube video: {str(e)}" @tool def get_current_time() -> str: """Get the current date and time in a readable format.""" now = datetime.now() return now.strftime("%Y-%m-%d %H:%M:%S") @tool def calculate_basic_math(expression: str) -> str: """ Safely evaluate basic mathematical expressions using AST. Args: expression: A string containing a mathematical expression like "2+2" or "10*5" Returns: The result of the calculation as a string """ # Safe operations mapping SAFE_OPS = { ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.Pow: op.pow, ast.USub: op.neg } def _safe_eval(node): """Recursively evaluate AST nodes safely.""" if isinstance(node, ast.Num): # Numbers return node.n elif isinstance(node, ast.Constant): # Python 3.8+ constant nodes return node.value elif isinstance(node, ast.BinOp): # Binary operations return SAFE_OPS[type(node.op)](_safe_eval(node.left), _safe_eval(node.right)) elif isinstance(node, ast.UnaryOp): # Unary operations return SAFE_OPS[type(node.op)](_safe_eval(node.operand)) else: raise ValueError(f"Unsupported operation: {type(node)}") try: # Parse expression into AST and evaluate safely node = ast.parse(expression, mode='eval') result = _safe_eval(node.body) return f"Result: {result}" except Exception as e: return f"Error calculating '{expression}': {str(e)}" @tool def save_note(content: str, filename: Optional[str] = None) -> str: """ Save a note to a text file. Args: content: The content to save in the note filename: Optional filename. If not provided, uses timestamp Returns: Success message with filename """ try: if filename is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"note_{timestamp}.txt" # Try to create notes directory, fall back to /tmp if permission denied try: os.makedirs("notes", exist_ok=True) notes_dir = "notes" except PermissionError: notes_dir = "/tmp/notes" os.makedirs(notes_dir, exist_ok=True) filepath = os.path.join(notes_dir, filename) with open(filepath, 'w', encoding='utf-8') as f: f.write(content) return f"✅ Note saved successfully to: {filepath}" except Exception as e: return f"❌ Error saving note: {str(e)}" @tool def list_saved_notes() -> str: """List all saved notes in the notes directory.""" try: # Check both possible notes directories all_files = [] for notes_dir in ["notes", "/tmp/notes"]: if os.path.exists(notes_dir): files = os.listdir(notes_dir) txt_files = [f for f in files if f.endswith('.txt')] all_files.extend([(f, notes_dir) for f in txt_files]) if not all_files: return "📂 No notes found. Save a note first!" file_list = "\n".join([f"- {file} ({location})" for file, location in sorted(all_files)]) return f"📂 Saved notes:\n{file_list}" except Exception as e: return f"❌ Error listing notes: {str(e)}" class FinalAnswerTool(Tool): name = "final_answer" description = "Provides a final answer to the given problem." inputs = {'answer': {'type': 'any', 'description': 'The final answer to the problem'}} output_type = "any" def forward(self, answer: Any) -> Any: return answer def __init__(self, *args, **kwargs): self.is_initialized = False