CoolFace
Apppublic

nynuzz/SamyAgent

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
tools.py748 linesDownload Raw Back to root
1import os
2import subprocess
3import base64
4import mimetypes
5from typing import List, Union
6from pathlib import Path
7import math
8import requests
9import pandas as pd
10import cv2
11from pytubefix import YouTube
12from youtube_transcript_api import YouTubeTranscriptApi
13from openai import OpenAI, APIError
14
15from langchain_core.tools import tool
16from langchain_community.document_loaders import CSVLoader
17
18from web_search_agent import web_search_graph
19
20
21#-----------------------------------------------------------------------------
22#   Helper Tools
23#-----------------------------------------------------------------------------
24@tool("sort_tool")
25def sort_tool(items: List[Union[float, str]], order: str = "ascending") -> Union[List[Union[float, str]], str]:
26    """
27    Sort a list of numbers (in numeric order) or strings (in alphabetical order).
28    Use this tool whenever you need to sort a list of items.
29
30    Args:
31        items (List[Union[float, str]]): The list of items to sort. The list must contain either only numbers or only strings.
32        order (str, optional): The sorting order. Valid values: 'ascending' or 'descending'. Default is 'ascending'.
33
34    Returns:
35        Union[List[Union[float, str]], str]: The sorted list if successful, or an error message string in case of failure.
36    """
37    print(f"--- ESECUZIONE DEL TOOL 'sort_list' CON INPUT: items={items}, order='{order}' ---")
38
39    # 1. Controlla se la lista è vuota
40    if not items:
41        return []
42
43    # 2. Valida l'argomento 'order' e imposta il flag per l'ordinamento
44    normalized_order = order.lower().strip()
45    if normalized_order == "ascending":
46        reverse_flag = False
47    elif normalized_order == "descending":
48        reverse_flag = True
49    else:
50        return "Error: The 'order' value is invalid. Allowed values are 'ascending' or 'descending'."
51
52    # 3. Esegui l'ordinamento, gestendo possibili errori di tipo
53    try:
54        # Usiamo la funzione built-in sorted(), che è molto efficiente
55        # e gestisce correttamente sia numeri che stringhe (ma non mischiati).
56        sorted_items = sorted(items, reverse=reverse_flag)
57        return sorted_items
58    except TypeError:
59        # Questa eccezione viene sollevata se si cerca di ordinare una lista
60        # con tipi non confrontabili, es. [1, "mela", 3]
61        return "Error: The list contains incompatible data types that cannot be sorted together (e.g., numbers and strings)."
62    except Exception as e:
63        return f"An unexpected error occurred during sorting: {e}"
64
65
66@tool("download_tool")
67def download_tool(task_id: str) -> Union[str, str]:
68    """
69    Download a file associated with a task_id from a predefined URL.
70    The file is saved in the 'uploads' directory with the name 'task_id.ext',
71    where the extension (.ext) is determined dynamically from the server response.
72
73    Args:
74        task_id (str): The unique identifier for the task and the file to download.
75
76    Returns:
77        Union[str, str]: The filename of the downloaded file if successful or an error message string in case of failure.
78    """
79    print(f"--- ESECUZIONE DEL TOOL 'download_file' CON INPUT: task_id={task_id} ---")
80
81    # 1. Impostazioni di base
82    BASE_URL = "https://agents-course-unit4-scoring.hf.space/files/"
83    UPLOADS_DIR = "./uploads/"
84    
85    # 2. Assicurarsi che la directory di destinazione esista
86    try:
87        os.makedirs(UPLOADS_DIR, exist_ok=True)
88    except OSError as e:
89        error_message = f"Error: Unable to create the destination directory '{UPLOADS_DIR}'. Details: {e}"
90        print(error_message)
91        return error_message
92
93    # 3. Eseguire la richiesta HTTP per scaricare il file
94    url = f"{BASE_URL}{task_id}"
95    try:
96        # Usare 'stream=True' è una buona pratica per scaricare file
97        with requests.get(url, stream=True, timeout=30) as response:
98            # Controlla se la richiesta ha avuto successo (es. status code 200)
99            response.raise_for_status()
100
101            # 4. Estrarre il nome del file originale per ottenere l'estensione
102            content_disposition = response.headers.get('content-disposition')
103            if not content_disposition:
104                error_message = "Error: The server response does not contain the 'content-disposition' header to get the file name."
105                print(error_message)
106                return error_message
107            
108            # Parsing dell'header per trovare il filename. Es: 'attachment; filename="nomefile.ext"'
109            parts = content_disposition.split(';')
110            filename_part = next((part for part in parts if 'filename=' in part), None)
111            
112            if not filename_part:
113                error_message = "Error: Unable to find 'filename' in the 'content-disposition' header."
114                print(error_message)
115                return error_message
116
117            original_filename = filename_part.split('=')[1].strip().strip('"')
118            _, extension = os.path.splitext(original_filename)
119
120            if not extension:
121                error_message = f"Error: Unable to get the file extension from '{original_filename}'."
122                print(error_message)
123                return error_message
124
125            # 5. Costruire il percorso di salvataggio e salvare il file
126            local_filename = f"{task_id}{extension}"
127            local_filepath = os.path.join(UPLOADS_DIR, local_filename)
128
129            with open(local_filepath, 'wb') as f:
130                # Scrive il contenuto a pezzi per gestire file di grandi dimensioni
131                for chunk in response.iter_content(chunk_size=8192):
132                    f.write(chunk)
133            
134            success_message = f"File scaricato con successo e salvato in: {local_filepath}"
135            print(success_message)
136            return local_filename
137
138    except requests.exceptions.RequestException as e:
139        # Gestisce errori di rete, timeout, DNS, etc.
140        error_message = f"Network error occurred while downloading the file: {e}"
141        print(error_message)
142        return error_message
143    except Exception as e:
144        # Cattura qualsiasi altra eccezione imprevista
145        error_message = f"An unexpected error occurred: {e}"
146        print(error_message)
147        return error_message
148    
149
150#-----------------------------------------------------------------------------
151#   Math Tools
152#-----------------------------------------------------------------------------
153@tool("add_tool")
154def add_tool(numbers: List[float]) -> float:
155    """
156    Calculate the sum of a list of numbers.
157    Use this tool when you need to perform a sum operation on multiple numbers.
158
159    Args:
160        numbers (List[float]): The list of numbers to be summed.
161    """
162    print(f"--- ESECUZIONE DEL TOOL 'sum_numbers' CON INPUT: {numbers} ---")
163    return sum(numbers)
164
165
166@tool("multiply_tool")
167def multiply_tool(numbers: List[float]) -> float:
168    """
169    Calculate the product of a list of numbers.
170    Use this tool when you need to multiply two or more numbers together.
171
172    Args:
173        numbers (List[float]): The list of numbers to be multiplied.
174    """
175    print(f"--- ESECUZIONE DEL TOOL 'multiply_numbers' CON INPUT: {numbers} ---")
176    if not numbers:
177        return 0
178    return math.prod(numbers)
179
180
181@tool("subtract_tool")
182def subtract_tool(minuend: float, subtrahend: float) -> float:
183    """
184    Calculate the subtraction between two numbers (minuend - subtrahend).
185    Use this tool to subtract one number from another.
186
187    Args:
188        minuend (float): The number from which to subtract (the first number).
189        subtrahend (float): The number to subtract (the second number).
190    """
191    print(f"--- ESECUZIONE DEL TOOL 'subtract_numbers' CON INPUT: minuend={minuend}, subtrahend={subtrahend} ---")
192    return minuend - subtrahend
193
194
195@tool("divide_tool")
196def divide_tool(dividend: float, divisor: float) -> Union[float, str]:
197    """
198    Calculate the division between two numbers (dividend / divisor).
199    Also handles the case of division by zero.
200
201    Args:
202        dividend (float): The number to be divided (the numerator).
203        divisor (float): The number to divide by (the denominator).
204    """
205    print(f"--- ESECUZIONE DEL TOOL 'divide_numbers' CON INPUT: dividend={dividend}, divisor={divisor} ---")
206    if divisor == 0:
207        return "Error: Division by zero is not allowed."
208    return dividend / divisor
209
210
211@tool("modulus_tool")
212def modulus_tool(dividend: float, divisor: float) -> Union[float, str]:
213    """
214    Calculate the remainder of the division between two numbers (dividend % divisor).
215    Use this tool when asked for the 'remainder' or the 'modulus' of a division.
216
217    Args:
218        dividend (float): The number being divided (the numerator).
219        divisor (float): The number by which to divide (the denominator).
220    """
221    print(f"--- ESECUZIONE DEL TOOL 'calculate_remainder' CON INPUT: dividend={dividend}, divisor={divisor} ---")
222    if divisor == 0:
223        return "Error: The divisor cannot be zero for the modulus operation."
224    return dividend % divisor
225
226
227@tool("power_tool")
228def power_tool(base: float, exponent: float) -> Union[float, str]:
229    """
230    Calculate a number raised to a power (base^exponent).
231    Use this tool for exponentiation operations.
232
233    Args:
234        base (float): The base of the operation.
235        exponent (float): The exponent to which the base is raised.
236    """
237    print(f"--- ESECUZIONE DEL TOOL 'calculate_power' CON INPUT: base={base}, exponent={exponent} ---")
238    try:
239        # Usiamo math.pow per coerenza e una migliore gestione degli errori
240        result = math.pow(base, exponent)
241        return result
242    except ValueError:
243        # Si verifica se, ad esempio, si cerca di calcolare (-4)^(0.5), che produce un numero complesso.
244        return "Error: Invalid operation. Ensure that the base and exponent do not result in a complex number (e.g., even root of a negative number)."
245
246
247@tool("square_root_tool")
248def square_root_tool(number: float) -> Union[float, str]:
249    """
250    Calculate the square root of a non-negative number.
251    Use this tool specifically to compute the square root.
252
253    Args:
254        number (float): The number for which to calculate the square root. Must be >= 0.
255    """
256    print(f"--- ESECUZIONE DEL TOOL 'square_root' CON INPUT: number={number} ---")
257    if number < 0:
258        return "Error: Cannot calculate the square root of a negative number."
259    return math.sqrt(number)
260
261
262#-----------------------------------------------------------------------------
263#   File Tools
264#-----------------------------------------------------------------------------
265@tool("tabular_tool")
266def tabular_tool(filename: str) -> Union[str, str]:
267    """
268    Analyze a local tabular data file (CSV, XLSX, XLS) and return its content
269    as a formatted string. For Excel files, each worksheet is processed individually.
270
271    Args:
272        filename (str): The filename of the CSV, XLSX, or XLS file to analyze.
273
274    Returns:
275        Union[str, str]: A formatted string containing the file's data, or an error message in case of issues.
276    """
277    print(f"--- ESECUZIONE DEL TOOL 'analyze_tabular_data' CON INPUT: filename='{filename}' ---")
278    UPLOADS_DIR = "./uploads/"
279    file_path = os.path.join(UPLOADS_DIR, filename)
280
281    # 1. Validazione dell'input: controlla se il file esiste
282    if not os.path.exists(file_path):
283        return f"Error: The file '{file_path}' was not found. Make sure it has been downloaded first."
284
285    try:
286        # 2. Determina il tipo di file e prepara la lista dei CSV da processare
287        file_extension = Path(file_path).suffix.lower()
288        csv_files_to_process = []
289        
290        # --- CASO 1: Il file è un Excel ---
291        if file_extension in ['.xlsx', '.xls']:
292            print(f"Rilevato file Excel. Inizio la conversione dei fogli in CSV temporanei...")
293            
294            # Legge tutti i fogli in un dizionario di DataFrame
295            excel_sheets = pd.read_excel(file_path, sheet_name=None)
296            
297            if not excel_sheets:
298                return f"Error: The Excel file '{file_path}' is empty or contains no worksheets."
299            
300            # Ottiene il nome base del file per i file temporanei
301            base_name = Path(file_path).stem
302            uploads_dir = Path(file_path).parent
303
304            for sheet_name, df in excel_sheets.items():
305                # Crea un nome di file sicuro per il CSV temporaneo
306                safe_sheet_name = "".join(c for c in sheet_name if c.isalnum() or c in (' ', '_')).rstrip()
307                temp_csv_path = uploads_dir / f"{base_name}_sheet_{safe_sheet_name}.csv"
308                
309                # Salva il DataFrame del foglio in un file CSV
310                df.to_csv(temp_csv_path, index=False)
311                print(f" - Foglio '{sheet_name}' convertito e salvato in: {temp_csv_path}")
312                csv_files_to_process.append(str(temp_csv_path))
313
314        # --- CASO 2: Il file è già un CSV ---
315        elif file_extension == '.csv':
316            print(f"Rilevato file CSV. Verrà processato direttamente.")
317            csv_files_to_process.append(file_path)
318
319        # --- CASO 3: Formato non supportato ---
320        else:
321            return f"Error: Unsupported file format '{file_extension}'. This tool supports only CSV, XLSX, and XLS."
322
323        # 3. Usa CSVLoader su tutti i file CSV identificati (originali o convertiti)
324        if not csv_files_to_process:
325            return "Error: No file to process was found."
326            
327        all_docs = []
328        for csv_path in csv_files_to_process:
329            loader = CSVLoader(file_path=csv_path)
330            docs = loader.load()
331            all_docs.extend(docs)
332        
333        # 4. Formatta l'output come richiesto
334        # Aumentiamo il limite di caratteri per dare più contesto all'LLM
335        formatted_output = "\n\n---\n\n".join(
336            [
337                f'<Document source="{Path(doc.metadata["source"]).name}" page="{doc.metadata.get("page", 0)}">\n{doc.page_content[:2500]}\n</Document>'
338                for doc in all_docs
339            ]
340        )
341        
342        print("Analisi completata con successo.")
343        return formatted_output
344
345    except Exception as e:
346        error_message = f"An unexpected error occurred while analyzing the file '{file_path}': {e}"
347        print(error_message)
348        return error_message
349
350
351@tool("audio_tool")
352def audio_tool(filename: str) -> Union[str, str]:
353    """
354    Transcribes a local audio file into text using OpenAI's Whisper model.
355    Use this tool when you need to extract the textual content from an audio file.
356    Supports common formats such as MP3, MP4, MPEG, MPGA, M4A, WAV, and WEBM.
357
358    Args:
359        filename (str): The filename of the audio file to transcribe.
360
361    Returns:
362        Union[str, str]: The transcribed text if successful, or an error message string in case of failure.
363    """
364    print(f"--- ESECUZIONE DEL TOOL 'transcribe_audio' CON INPUT: file_path='{filename}' ---")
365    UPLOADS_DIR = "./uploads/"
366    file_path = os.path.join(UPLOADS_DIR, filename)
367    client = OpenAI()
368
369    # 1. Controlla se il client è stato inizializzato correttamente
370    if client is None:
371        return "Error: The OpenAI client is not configured. Please check your API key."
372
373    # 2. Controlla se il file esiste prima di tentare di aprirlo
374    if not os.path.exists(file_path):
375        return f"Error: The file '{file_path}' was not found. Make sure it has been downloaded first."
376
377    try:
378        # 3. Apri il file in modalità binaria e invialo all'API di OpenAI
379        with open(file_path, "rb") as audio_file:
380            transcription = client.audio.transcriptions.create(
381                model="whisper-1",
382                file=audio_file
383            )
384        
385        print("Trascrizione completata con successo.")
386        # La risposta dell'API contiene il testo nel campo 'text'
387        return transcription.text
388
389    except APIError as e:
390        # Gestisce errori specifici dell'API di OpenAI (es. file non valido, auth error)
391        error_message = f"Error from the OpenAI API during transcription: {e}"
392        print(error_message)
393        return error_message
394        
395    except Exception as e:
396        # Gestisce altri errori imprevisti (es. problemi di lettura del file)
397        error_message = f"An unexpected error occurred while transcribing the file '{file_path}': {e}"
398        print(error_message)
399        return error_message
400    
401
402@tool("image_tool")
403def image_tool(filename: str, user_question: str) -> Union[str, str]:
404    """
405    Reads a local image file and encodes it in base64 format, ready to be analyzed by a multimodal model (such as GPT-4o).
406    Use this tool to prepare any image (JPG, PNG, WEBP, etc.) before asking questions about its content.
407
408    Args:
409        filename (str): The filename of the image file to prepare.
410        user_question (str): The user's original question to guide the analysis.
411
412    Returns:
413        Union[str, str]: A textual analysis based on the base64 encoded image data, or an error message string.
414    """
415    print(f"--- ESECUZIONE DEL TOOL 'prepare_image_for_analysis' CON INPUT: file_path='{filename}' ---")
416    UPLOADS_DIR = "./uploads/"
417    file_path = os.path.join(UPLOADS_DIR, filename)
418    client = OpenAI()
419
420    # 1. Controlla se il file esiste
421    if not os.path.exists(file_path):
422        return f"Error: The image file '{file_path}' was not found."
423
424    try:
425        # 2. Determina il tipo MIME dell'immagine (es. 'image/jpeg', 'image/png')
426        mime_type, _ = mimetypes.guess_type(file_path)
427        if not mime_type or not mime_type.startswith('image/'):
428            return f"Error: The file '{file_path}' is not a supported image format."
429
430        # 3. Leggi il file in modalità binaria e codificalo in base64
431        with open(file_path, "rb") as image_file:
432            encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
433
434        # 4. Formatta l'output come un data URI, il formato standard per passare immagini
435        #    a modelli multimodali.
436        image_data = f"data:{mime_type};base64,{encoded_string}"
437        # Crea il prompt per l'analisi
438        analysis_prompt = [
439            {
440                "role": "user",
441                "content": [
442                    {
443                        "type": "text",
444                        "text": f"""
445                            You are an expert visual analyst. Your task is to describe the provided image in extreme detail to help answer the user's question.
446                            Focus on the elements relevant to the question. Be objective and precise.
447
448                            **User's Question:** '{user_question}'
449
450                            Analyze the image and provide a detailed description.
451                        """
452                    },
453                    {
454                        "type": "image_url",
455                        "image_url": {
456                            "url": image_data,
457                            "detail": "high" # Usa alta risoluzione per la massima precisione
458                        }
459                    }
460                ]
461            }
462        ]
463        
464
465        # 5. Restituisce la risposta in base all'immagine analizzata.
466        # Chiama l'API di OpenAI
467        response = client.chat.completions.create(
468            model="gpt-4o-mini",  # o "gpt-4-vision-preview"
469            messages=analysis_prompt,
470            max_tokens=1000,
471            temperature=0
472        )
473        
474        description = response.choices[0].message.content
475        print("--- Image analysis complete. ---")
476        return description
477    except Exception as e:
478        return f"An error occurred during the visual analysis: {e}"    
479
480
481#-----------------------------------------------------------------------------
482#   Code Execution Tools
483#-----------------------------------------------------------------------------
484@tool("code_writer_tool")
485def code_writer_tool(code: str, task_id: str) -> str:
486    """
487    Writes a string of Python code to a local file. This is the first step
488    for any task that requires writing and then executing code. The task_id is the name for the file.
489
490    Args:
491        code (str): A string containing the complete, valid Python code to be written to the file.
492        task_id (str): The name for the file.
493
494    Returns:
495        local_filename (str): The local filename of python file to execute.
496    """
497    print(f"--- TOOL: Writing code to file: {task_id}.py ---")
498    UPLOADS_DIR = "./uploads/"
499    local_filename = f"{task_id}.py"
500    file_path = os.path.join(UPLOADS_DIR, local_filename)    
501
502    try:
503        # Scrive il codice nel file
504        with open(file_path, "w", encoding="utf-8") as f:
505            f.write(code)
506        
507        success_message = f"Successfully wrote code to {file_path}."
508        print(success_message)
509        
510        # Restituisce il percorso del file, che servirà al tool di esecuzione
511        return local_filename
512    except Exception as e:
513        error_message = f"An error occurred while writing the file: {e}"
514        print(error_message)
515        return error_message
516    
517
518@tool("code_tool")
519def code_tool(filename: str, timeout_seconds: int = 100) -> Union[str, dict]:
520    """
521    Executes a programming code file in an isolated and secure environment, capturing its standard output and errors.
522    Use this tool to run programming code when you need to analyze its behavior or output.
523
524    Args:
525        filename (str): The filename of the code file to execute.
526        timeout_seconds (int, optional): The maximum number of seconds the execution is allowed to run before forcibly terminating the process. Default is 10.
527
528    Returns:
529        Union[str, dict]: A dictionary containing 'stdout', 'stderr', and 'return_code' if successful, or an error message string if the tool itself fails.
530    """
531    print(f"--- ESECUZIONE DEL TOOL 'execute_python_file' SU: {filename} ---")
532    UPLOADS_DIR = "./uploads/"
533    file_path = os.path.join(UPLOADS_DIR, filename)
534
535    # 1. Controlla di sicurezza: il file esiste?
536    if not os.path.exists(file_path):
537        return f"Error: The code file '{file_path}' was not found."
538
539    # 2. Usa subprocess.run per eseguire il codice in modo sicuro
540    try:
541        # 'subprocess.run' è il modo moderno e raccomandato per eseguire processi
542        process = subprocess.run(
543            ['python', file_path],     # Il comando da eseguire (es. 'python nomefile.py')
544            capture_output=True,       # Cattura stdout e stderr
545            text=True,                 # Decodifica stdout/stderr come testo (UTF-8)
546            timeout=timeout_seconds    # Imposta un timeout
547        )
548
549        execution_result = {
550            "return_code": process.returncode,
551            "stdout": process.stdout.strip(),
552            "stderr": process.stderr.strip()
553        }
554
555        # 3. Ritorna un dizionario strutturato con i risultati
556        return execution_result
557
558    except FileNotFoundError:
559        # Questo errore si verifica se l'interprete 'python' non è nel PATH del sistema
560        return "Error: The 'python' interpreter was not found on the system. Unable to execute the code."
561    except subprocess.TimeoutExpired as e:
562        # Gestisce il caso in cui il codice va in timeout
563        return {
564            "return_code": -1, # Codice di ritorno personalizzato per timeout
565            "stdout": e.stdout.strip() if e.stdout else "",
566            "stderr": f"Error: Execution terminated after {timeout_seconds} seconds (Timeout)."
567        }
568    except Exception as e:
569        # Cattura qualsiasi altro errore imprevisto durante l'esecuzione del tool
570        return f"An unexpected error occurred while executing the tool: {e}"
571    
572
573#-----------------------------------------------------------------------------
574#   Youtube Video Tools
575#-----------------------------------------------------------------------------
576@tool("youtube_info_tool")
577def youtube_info_tool(youtube_url: str) -> Union[str, dict]:
578    """
579    Collects information and resources from a YouTube video. Downloads both audio and video, and retrieves the official transcript if available.
580    This is ALWAYS the first tool to call when working with a YouTube video.
581
582    Args:
583        youtube_url (str): The full URL of the YouTube video.
584
585    Returns:
586        Union[str, dict]: A dictionary with the collected resources (transcript, audio_filename, video_filename) or an error message.
587    """
588    print(f"--- ESECUZIONE DEL TOOL 'get_youtube_video_info' CON URL: {youtube_url} ---")
589    UPLOADS_DIR = "./uploads/"
590
591    try:
592        yt = YouTube(youtube_url)
593        video_id = yt.video_id
594        
595        # 1. Recupera la trascrizione ufficiale
596        transcript_text = None
597        try:
598            transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
599            transcript_text = " ".join([d['text'] for d in transcript_list])
600            print("Trascrizione ufficiale trovata.")
601        except Exception:
602            print("Nessuna trascrizione ufficiale disponibile.")
603
604        # 2. Scarica l'audio
605        audio_stream = yt.streams.get_audio_only()
606        audio_path = audio_stream.download(output_path=UPLOADS_DIR, filename=f"{video_id}.m4a")
607        if transcript_text is None:
608            transcript_text = audio_tool.invoke({"filename":f"{video_id}.m4a"})
609        print(f"Audio scaricato in: {audio_path}")
610
611        # 3. Scarica il video
612        video_stream = yt.streams.get_highest_resolution()
613        video_path = video_stream.download(output_path=UPLOADS_DIR, filename=f"{video_id}.mp4")
614        print(f"Video scaricato in: {video_path}")
615
616        video_info = {
617            "title": yt.title,
618            "description": yt.description,
619            "transcript": transcript_text,
620            "audio_filename": f"{video_id}.m4a",
621            "video_filename": f"{video_id}.mp4"
622        }
623        
624        return video_info
625    except Exception as e:
626        return f"Error while retrieving information from the YouTube video: {e}"
627    
628
629@tool("youtube_frame_tool")
630def youtube_frame_tool(filename: str, title: str, description: str, transcript: str, user_question: str, sample_rate_seconds: int = 5) -> Union[str, str]:
631    """
632    Analyzes video content by combining visual information from frames with the provided transcript to answer a specific user question.
633    To be used as a last resort, when the transcript and audio are not sufficient, or for purely visual questions.
634
635    Args:
636        filename (str): The filename of the video file.
637        title (str): The title of the video.
638        description (str): A brief description of the video.
639        transcript (str): The full text transcript of the video (either official or from audio).
640        user_question (str): The user's original question to guide the analysis.
641        sample_rate_seconds (int): Interval in seconds between frames to analyze. Default is 3.
642
643    Returns:
644        Union[str, str]: A textual analysis based on the video frames or an error message.
645    """
646    print(f"--- ESECUZIONE DEL TOOL 'analyze_video_frames' SU: {filename} ---")
647    UPLOADS_DIR = "./uploads/"
648    video_path = os.path.join(UPLOADS_DIR, filename)
649    client = OpenAI()
650    
651    if not os.path.exists(video_path):
652        return f"Error: Video file not found at '{video_path}'."
653    if client is None:
654        return "Error: The OpenAI client is not configured."
655
656    video = cv2.VideoCapture(video_path)
657    fps = video.get(cv2.CAP_PROP_FPS)
658    frame_interval = int(fps * sample_rate_seconds)
659    
660    base64_frames = []
661    frame_count = 0
662    
663    while video.isOpened():
664        success, frame = video.read()
665        if not success:
666            break
667        
668        if frame_count % frame_interval == 0:
669            _, buffer = cv2.imencode(".jpg", frame)
670            base64_frames.append(base64.b64encode(buffer).decode("utf-8"))
671        
672        frame_count += 1
673
674    video.release()
675    print(f"Campionati {len(base64_frames)} frame dal video.")
676
677    if not base64_frames:
678        return "Error: Unable to extract frames from the video."
679
680    prompt_messages = [
681        {
682            "role": "user",
683            "content": [
684                {
685                    "type": "text",
686                    "text": f"""
687                        You are a video content analyst.
688                        Your task is to answer the user's question by combining information from different sources: 
689                        - the title
690                        - the description
691                        - the transcript
692                        - a series of sampled frames
693                        **IMPORTANT**: Analyze the all sources of the video in great detail, because there may be important information to solve the task.
694                        
695                        **User Question**: {user_question}
696                        **Video Title**: {title}
697                        **Video Description**: {description}
698                        **Video Transcript**: {transcript if transcript else "No transcript available."}
699                        
700                        Begin your rigorous analysis now. Here are the frames:
701                    """
702                },
703                # Inserimento dei Frame
704                *map(lambda x: {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{x}", "detail": "low"}}, base64_frames),
705            ],
706        }
707    ]
708
709    try:
710        response = client.chat.completions.create(
711            model="gpt-4o-mini",
712            temperature=0,
713            messages=prompt_messages,
714            max_tokens=1000,
715        )
716        analysis_summary = response.choices[0].message.content
717        return analysis_summary
718    except Exception as e:
719        return f"Error while analyzing frames with the OpenAI API: {e}"
720    
721
722#-----------------------------------------------------------------------------
723#   Web Search Tools
724#-----------------------------------------------------------------------------
725@tool("web_search_tool")
726def web_search_tool(task: str) -> str:
727    """
728    Delegates complex research tasks to a specialized, cyclic research agent.
729    Use this for any question that requires external, up-to-date, or detailed knowledge.
730    """
731    print(f"--- MAIN AGENT: DELEGATING RESEARCH FOR: '{task}' ---")
732    
733    # Lo stato iniziale ora contiene il task e un primo messaggio umano vuoto per avviare il ciclo.
734    # L'agente di ricerca leggerà il task dallo stato e ignorerà questo messaggio.
735    initial_state = {"task": task, "context_summary": ""}
736    
737    # Esegui il sub-grafo
738    final_state = web_search_graph.invoke(initial_state)
739    
740    # Il risultato finale è l'ultimo messaggio nella cronologia, che sarà la risposta del writer.
741    final_answer = final_state["messages"][-1].content
742    return final_answer
743
744
745assistant_tools_list = [
746    sort_tool, download_tool, add_tool, multiply_tool, subtract_tool, divide_tool, modulus_tool, tabular_tool, audio_tool, image_tool, code_writer_tool, code_tool, youtube_info_tool, youtube_frame_tool, web_search_tool
747]
748