gdms commited on
Commit
30944a6
·
1 Parent(s): 1542c35

Montagem arquitetura

Browse files
.gitignore CHANGED
@@ -1,5 +1,3 @@
1
- *.mp3
2
- *.xlsx
3
  video_analysis_output/
4
  get-pip.py
5
  *.m4a
 
 
 
1
  video_analysis_output/
2
  get-pip.py
3
  *.m4a
agent.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.prebuilt import create_react_agent
2
+ from prompts import *
3
+ from tools import *
4
+ from langgraph_supervisor import create_supervisor
5
+ from langchain.chat_models import init_chat_model
6
+
7
+ import glob
8
+
9
+ class Agent:
10
+ def __init__(self):
11
+ print("Initializing Agent....")
12
+
13
+ print("--> Audio Agent")
14
+ self.audio_agent = create_react_agent(
15
+ model="openai:gpt-4o-mini", # gpt-4o-mini-2024-07-18
16
+ tools=[extract_text_from_url_tool, extract_text_from_file_tool],
17
+ prompt= AUDIO_AGENT_PROMPT,
18
+ name="audio_agent",
19
+ )
20
+
21
+ print("--> Web Search Agent")
22
+ self.web_search_agent = create_react_agent(
23
+ model="openai:gpt-4o-mini", # gpt-4o-mini-2024-07-18
24
+ tools=[search_web_tool],
25
+ prompt= WEB_SEARCH_AGENT_PROMPT,
26
+ name="web_research_agent",
27
+ )
28
+
29
+ print("--> Supervisor")
30
+ self.supervisor = create_supervisor(
31
+ model=init_chat_model("openai:gpt-4o-mini"),
32
+ agents=[self.web_search_agent, self.audio_agent],
33
+ tools=[bird_video_count_tool,chess_image_to_fen_tool,chess_fen_get_best_next_move_tool,
34
+ get_excel_columns_tool, calculate_excel_sum_by_columns_tool,execute_python_code_tool,
35
+ text_inverter_tool, check_table_commutativity_tool],
36
+ prompt= SUPERVISOR_PROMPT,
37
+ add_handoff_back_messages=True,
38
+ output_mode="full_history",
39
+ ).compile()
40
+
41
+ print("Agent initialized.")
42
+
43
+ def _exist_file_with_task_id(task_id: str) -> str:
44
+ padrao = os.path.join(AGENTS_FILES_PATH, f"{task_id}.*")
45
+ arquivos_encontrados = glob.glob(padrao)
46
+ return arquivos_encontrados[0] if arquivos_encontrados else None
47
+
48
+
49
+ def __call__(self, question: str, task_id: str) -> str:
50
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
51
+ file = self._exist_file_with_task_id(task_id)
52
+
53
+ fixed_answer = "This is a default answer."
54
+ print(f"Agent returning fixed answer: {fixed_answer}")
55
+ return fixed_answer
agent_util.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.messages import convert_to_messages
2
+
3
+
4
+ class Agent_Util:
5
+ @staticmethod
6
+ def pretty_print_message(message, indent=False):
7
+ pretty_message = message.pretty_repr(html=True)
8
+ if not indent:
9
+ print(pretty_message)
10
+ return
11
+
12
+ indented = "\n".join("\t" + c for c in pretty_message.split("\n"))
13
+ print(indented)
14
+
15
+ @staticmethod
16
+ def pretty_print_messages(update, last_message=False):
17
+ is_subgraph = False
18
+ if isinstance(update, tuple):
19
+ ns, update = update
20
+ # skip parent graph updates in the printouts
21
+ if len(ns) == 0:
22
+ return
23
+
24
+ graph_id = ns[-1].split(":")[0]
25
+ print(f"Update from subgraph {graph_id}:")
26
+ print("\n")
27
+ is_subgraph = True
28
+
29
+ for node_name, node_update in update.items():
30
+ update_label = f"Update from node {node_name}:"
31
+ if is_subgraph:
32
+ update_label = "\t" + update_label
33
+
34
+ print(update_label)
35
+ print("\n")
36
+
37
+ messages = convert_to_messages(node_update["messages"])
38
+ if last_message:
39
+ messages = messages[-1:]
40
+
41
+ for m in messages:
42
+ Agent_Util.pretty_print_message(m, indent=is_subgraph)
43
+ print("\n")
app.py CHANGED
@@ -4,6 +4,61 @@ import requests
4
  import inspect
5
  import pandas as pd
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  # (Keep Constants as is)
8
  # --- Constants ---
9
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
 
4
  import inspect
5
  import pandas as pd
6
 
7
+ import getpass
8
+ import os
9
+ import requests
10
+ from typing import Tuple, List, Dict, Any, Optional, ClassVar, Literal
11
+ import json
12
+
13
+ import re
14
+
15
+ import sys
16
+ import io
17
+ import traceback
18
+ from contextlib import redirect_stdout, redirect_stderr
19
+
20
+ from openai import OpenAI
21
+ import pandas as pd
22
+
23
+
24
+ #Multi-agent
25
+ from langchain_tavily import TavilySearch, TavilyExtract
26
+ from langgraph.prebuilt import create_react_agent
27
+ from langgraph_supervisor import create_supervisor
28
+ from langchain.chat_models import init_chat_model
29
+
30
+ #langgraph
31
+ from pydantic import BaseModel
32
+ from langgraph.graph import START, StateGraph, MessagesState
33
+ from langgraph.prebuilt import tools_condition, ToolNode
34
+ from IPython.display import Image, display
35
+ from langchain.tools import tool
36
+ from langgraph.prebuilt import create_react_agent
37
+ from langchain_core.messages import convert_to_messages
38
+ from langsmith import utils
39
+
40
+ #web tool
41
+ from tavily import TavilyClient
42
+ import markdownify
43
+ import wikipediaapi
44
+ from datetime import datetime, timezone
45
+ import urllib.parse
46
+ from bs4 import BeautifulSoup
47
+ import time
48
+
49
+ #audio
50
+ import shutil
51
+ import subprocess
52
+
53
+ #video
54
+ import cv2
55
+
56
+ #imagem
57
+ import base64
58
+ import mimetypes
59
+ from urllib.parse import urlparse, unquote
60
+ import google.generativeai as genai
61
+
62
  # (Keep Constants as is)
63
  # --- Constants ---
64
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
audio_util.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import subprocess
4
+
5
+ from openai import OpenAI
6
+ from constantes import YOUTUBE_COOKIE_PATH
7
+ from file_util import File_Util
8
+
9
+
10
+ class Audio_Util:
11
+ """
12
+ Manipulação de audio
13
+ """
14
+ @staticmethod
15
+ def download_audio_from_url(url: str, output_path: str, audio_file_name: str) -> str:
16
+ """
17
+ Baixa um arquivo de áudio a partir de uma URL.
18
+ Args:
19
+ url: url do audio
20
+ output_path: local esperado para gravação do audio
21
+ audio_file_name: nome do arquivo que deve ser utilizado para download
22
+ """
23
+
24
+ audio_path = f'{output_path}/{audio_file_name}.%(ext)s'
25
+
26
+ print(f"Baixando áudio de {url} para {audio_path}...")
27
+ try:
28
+ # Comando yt-dlp para baixar o melhor áudio disponível e convertê-lo para mp3
29
+ command = [
30
+ 'yt-dlp',
31
+ "--cookies", YOUTUBE_COOKIE_PATH,
32
+ '-f', 'bestaudio[ext=m4a]',
33
+ '-o', audio_path,
34
+ url
35
+ ]
36
+
37
+ result = subprocess.run(command, check=True, capture_output=True, text=True)
38
+ lista_arquivos = File_Util.retirar_sufixo_codec_arquivo(output_path)
39
+ print("Download de áudio concluído com sucesso.")
40
+ return f"{output_path}/{lista_arquivos[0]}"
41
+ except subprocess.CalledProcessError as e:
42
+ print(f"Erro ao baixar o áudio: {e}")
43
+ print(f"Saída do erro: {e.stderr}")
44
+ return False
45
+ except FileNotFoundError:
46
+ print("Erro: O comando 'yt-dlp' não foi encontrado. Certifique-se de que ele está instalado e no PATH do sistema.")
47
+ return False
48
+
49
+
50
+ @staticmethod
51
+ def extract_text_from_audio_file(audio_path: str) -> str:
52
+ """
53
+ Usa a API Whisper da OpenAI para transcrever o áudio em texto com quebras de linha naturais,
54
+ removendo timestamps e IDs. Salva em arquivo .txt se o caminho for fornecido.
55
+ """
56
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
57
+
58
+ try:
59
+ audio_path = f"{audio_path}"
60
+ print(f"Iniciando transcrição (formato SRT simplificado): {audio_path}")
61
+
62
+ with open(audio_path, "rb") as audio_file:
63
+ transcription = client.audio.transcriptions.create(
64
+ model="whisper-1",
65
+ file=audio_file,
66
+ response_format="srt"
67
+ )
68
+
69
+ # Remove linhas com números e timestamps
70
+ lines = transcription.splitlines()
71
+ only_text = [line.strip() for line in lines if not re.match(r"^\d+$", line) and "-->" not in line]
72
+ formatted_text = "\n".join(only_text)
73
+
74
+ return formatted_text
75
+ except Exception as e:
76
+ print(f"Erro ao transcrever áudio: {e}")
77
+ return ""
78
+
79
+
constantes.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+
4
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
5
+ TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
6
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
7
+ LANGCHAIN_API_KEY = os.getenv("LANGCHAIN_API_KEY")
8
+ os.environ["LANGCHAIN_TRACING_V2"] = "true"
9
+ os.environ["LANGCHAIN_PROJECT"] = "hf-final-assessment"
10
+
11
+ OUTPUT_AUDIO_PATH ="./audio_analysis_output"
12
+ AUDIO_FILENAME = "downloaded_audio"
13
+ TRANSCRIPT_FILENAME = "transcript.txt"
14
+
15
+ OUTPUT_IMAGE_PATH = "./image_analysis_output"
16
+ IMAGE_FILE_NAME = "downloaded_image"
17
+
18
+ OUTPUT_VIDEO_PATH = "./video_analysis_output"
19
+ VIDEO_FILE_NAME = "downloaded_video"
20
+ FRAME_INTERVAL_SECONDS = 0.5 # Intervalo entre frames a serem extraídos
21
+ INICIO_FRAME_IMPORTANTE = 191 # inicio intervalo relevante, para não ficar caro a inferencia ao gpt
22
+ FIM_FRAME_IMPORTANTE = 193# fim intervalo relevante, para não ficar caro a inferencia ao gpt
23
+ YOUTUBE_COOKIE_PATH = "./support/cookie-youtube.txt"
24
+
25
+ GPT_IMAGE_MODEL = "gpt-4o"
26
+ GEMINI_MODEL = "gemini-2.0-flash"
27
+ CHESSVISION_TO_FEN_URL = "http://app.chessvision.ai/predict"
28
+ CHESS_MOVE_API = "https://chess-api.com/v1"
29
+
30
+ AGENTS_FILES_PATH = "./files"
file_util.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import shutil
4
+ from typing import List
5
+
6
+ from constantes import AGENTS_FILES_PATH
7
+
8
+
9
+
10
+
11
+ class File_Util:
12
+ """
13
+ Manipulação de diretórios e arquivos
14
+ """
15
+
16
+ @staticmethod
17
+ def create_or_clear_output_directory(output_dir: str):
18
+ """
19
+ Cria o diretório de saída se não existir.
20
+ """
21
+ if not os.path.exists(output_dir):
22
+ os.makedirs(output_dir)
23
+ print(f"Diretório criado: {output_dir}")
24
+ else:
25
+ # Limpa todos os arquivos e subdiretórios
26
+ for filename in os.listdir(output_dir):
27
+ file_path = os.path.join(output_dir, filename)
28
+ try:
29
+ if os.path.isfile(file_path) or os.path.islink(file_path):
30
+ os.unlink(file_path)
31
+ elif os.path.isdir(file_path):
32
+ shutil.rmtree(file_path)
33
+ except Exception as e:
34
+ print(f"Erro ao excluir {file_path}: {e}")
35
+ print(f"Diretório limpo: {output_dir}")
36
+
37
+ @staticmethod
38
+ def retirar_sufixo_codec_arquivo(directory: str) -> List[str]:
39
+ """
40
+ Os arquivos de audio e video quando baixados ficam com o codec
41
+ embutido no nome, dificultando identificar o nome do arquivo a ser
42
+ processado. O objetivo é remover o sufixo do nome do arquivo.
43
+ """
44
+ return_list = []
45
+
46
+ for filename in os.listdir(directory):
47
+ # Procura padrão como ".f123" antes da extensão
48
+ new_filename = re.sub(r'\.f\d{3}(?=\.\w+$)', '', filename)
49
+ if new_filename != filename:
50
+ old_path = os.path.join(directory, filename)
51
+ new_path = os.path.join(directory, new_filename)
52
+ os.rename(old_path, new_path)
53
+ print(f"Renomeado: {filename} → {new_filename}")
54
+ return_list.append(new_filename)
55
+ return return_list
56
+
57
+
58
+ @staticmethod
59
+ def tratar_arquivo_local(caminho_entrada: str) -> str:
60
+ """
61
+ Verifica se o arquivo existe no caminho informado.
62
+ Se não existir, adiciona o path padrão do sistema.
63
+
64
+ Parâmetros:
65
+ - caminho_entrada (str): Caminho completo ou nome do arquivo.
66
+
67
+ Retorna:
68
+ - str: Caminho válido para o arquivo, ou None se não encontrado.
69
+ """
70
+ # Verifica se o arquivo já existe no caminho informado
71
+ if os.path.isfile(caminho_entrada):
72
+ return caminho_entrada
73
+
74
+ # Tenta procurar no diretório padrão, se fornecido
75
+ novo_caminho = os.path.join(AGENTS_FILES_PATH, os.path.basename(caminho_entrada))
76
+ if os.path.isfile(novo_caminho):
77
+ return novo_caminho
78
+
79
+ # Arquivo não encontrado
80
+ return None
image_util.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import mimetypes
3
+ import os
4
+ from urllib.parse import unquote, urlparse
5
+
6
+ import requests
7
+ from file_util import File_Util
8
+
9
+
10
+ class Image_Util:
11
+ """
12
+ Manipulação de imagens
13
+ """
14
+ @staticmethod
15
+ def encode_image_to_base64(image_path: str) -> str:
16
+ """Codifica um arquivo de imagem (frame) para base64."""
17
+
18
+ image_path_tratado = File_Util.tratar_arquivo_local(image_path)
19
+ if not image_path_tratado:
20
+ return None
21
+ try:
22
+ with open(image_path_tratado, "rb") as image_file:
23
+ return base64.b64encode(image_file.read()).decode('utf-8')
24
+ except FileNotFoundError:
25
+ print(f"Erro: Arquivo não encontrado em {image_path}")
26
+ return None
27
+ except Exception as e:
28
+ print(f"Erro ao codificar imagem {image_path} para base64: {e}")
29
+ return None
30
+
31
+ @staticmethod
32
+ def get_image_extension_from_url(url: str) -> str:
33
+ """
34
+ Retorna a extensão do arquivo de imagem com base na URL informada.
35
+
36
+ Args:
37
+ url: URL da imagem (pode conter parâmetros).
38
+
39
+ Returns:
40
+ Extensão do arquivo (ex: 'jpg', 'png') ou None se não for possível identificar.
41
+ """
42
+
43
+ path = unquote(urlparse(url).path) # decodifica e extrai o caminho da URL
44
+ filename = os.path.basename(path)
45
+
46
+ # Tenta extrair extensão diretamente
47
+ _, ext = os.path.splitext(filename)
48
+ ext = ext.lower().lstrip('.') # remove o ponto
49
+
50
+ # Verifica se a extensão é de imagem conhecida
51
+ if ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff']:
52
+ return ext
53
+
54
+ # Caso não haja extensão, tenta deduzir pelo tipo MIME
55
+ mime_type, _ = mimetypes.guess_type(url)
56
+ if mime_type and mime_type.startswith("image/"):
57
+ return mime_type.split("/")[1] # ex: 'image/png' → 'png'
58
+
59
+ return None
60
+
61
+ @staticmethod
62
+ def download_image_from_url(url: str, output_path: str, image_file_name: str) -> str:
63
+ """
64
+ Baixa uma imagem a partir de uma URL.
65
+ Args:
66
+ url: url da imagem
67
+ output_path: local esperado para gravação da imagem
68
+ image_file_name: nome do arquivo que deve ser utilizado para download
69
+ """
70
+ File_Util.create_or_clear_output_directory(output_path)
71
+ image_path = f'{output_path}/{image_file_name}.{Image_Util.get_image_extension_from_url(url)}'
72
+ response = requests.get(url, stream=True)
73
+
74
+ if response.status_code == 200:
75
+ if save_path is None:
76
+ save_path = os.path.basename(url.split("?")[0]) # remove query params, se houver
77
+
78
+ with open(save_path, 'wb') as f:
79
+ for chunk in response.iter_content(1024):
80
+ f.write(chunk)
81
+ return save_path
82
+ else:
83
+ raise Exception(f"Erro ao baixar imagem: {response.status_code} - {response.reason}")
84
+
85
+ return image_path
prompts.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ AUDIO_AGENT_PROMPT = (
2
+ "You are an audio agent.\n\n"
3
+ "INSTRUCTIONS:\n"
4
+ "- Assist ONLY with audio-related tasks, DO NOT do any math\n"
5
+ "- If you get an audio request related to a file, use the file name to call the tools, they know the path to find the file. \n"
6
+ "- Its tools can even extract text from videos on the internet \n"
7
+ "- After you're done with your tasks, respond to the supervisor directly\n"
8
+ "- Respond ONLY with the results of your work, do NOT include ANY other text."
9
+ )
10
+
11
+ WEB_SEARCH_AGENT_PROMPT = (
12
+ "You are a web research agent.\n\n"
13
+ "INSTRUCTIONS:\n"
14
+ "- Assist ONLY with research-related tasks, DO NOT do any math\n"
15
+ "- After you're done with your tasks, respond to the supervisor directly\n"
16
+ "- Respond ONLY with the results of your work, do NOT include ANY other text."
17
+ )
18
+
19
+ SUPERVISOR_PROMPT = (
20
+ """
21
+ You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template:
22
+ FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
23
+ If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
24
+ If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
25
+ If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
26
+ To assist in your task, you can supervise other agents who perform specific tasks that could not be handled by tools, since they require the processing of another LLM. Below, I will inform you about your assistants:
27
+ - web_research_agent. Assign web research related tasks to this agent
28
+ - audio_agent. Assign audio related tasks to this agent. This agent can extract text from videos in files or on the internet.
29
+ Assign work to one agent at a time, do not call agents in parallel.
30
+ Priorize the use of tools and another agents to help in reasoning.
31
+ When a file or URL is entered at the prompt, use it in tools or other agents, both are prepared to handle files and URLs.
32
+ When I inform a file in the format File:<file name> use the file name to invoke the tool, it will know how to treat it.
33
+ """
34
+ )
question_files/cca530fc-4052-43b2-b130-b30968d8aa44.png DELETED
Binary file (63.1 kB)
 
question_files/f918266a-b3e0-4914-865d-4faa564f1aef.py DELETED
@@ -1,35 +0,0 @@
1
- from random import randint
2
- import time
3
-
4
- class UhOh(Exception):
5
- pass
6
-
7
- class Hmm:
8
- def __init__(self):
9
- self.value = randint(-100, 100)
10
-
11
- def Yeah(self):
12
- if self.value == 0:
13
- return True
14
- else:
15
- raise UhOh()
16
-
17
- def Okay():
18
- while True:
19
- yield Hmm()
20
-
21
- def keep_trying(go, first_try=True):
22
- maybe = next(go)
23
- try:
24
- if maybe.Yeah():
25
- return maybe.value
26
- except UhOh:
27
- if first_try:
28
- print("Working...")
29
- print("Please wait patiently...")
30
- time.sleep(0.1)
31
- return keep_trying(go, first_try=False)
32
-
33
- if __name__ == "__main__":
34
- go = Okay()
35
- print(f"{keep_trying(go)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -6,4 +6,5 @@ bs4
6
  tavily-python
7
  markdownify
8
  wikipedia-api
9
- yt-dlp
 
 
6
  tavily-python
7
  markdownify
8
  wikipedia-api
9
+ yt-dlp
10
+ ipython
tools.py ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import redirect_stderr, redirect_stdout
2
+ import io
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import traceback
7
+ from typing import Dict, List, Literal, Optional
8
+
9
+ import google.generativeai as genai
10
+
11
+ import cv2
12
+ import pandas as pd
13
+ from pydantic import BaseModel
14
+ import requests
15
+ from audio_util import Audio_Util
16
+ from constantes import *
17
+ from file_util import File_Util
18
+ from image_util import Image_Util
19
+
20
+ from tavily import TavilyClient
21
+
22
+ from web_util import Web_Util
23
+ from wikipedia_util import Wikipedia_Historical_Page, Wikipedia_Util
24
+
25
+
26
+ class Video_Util:
27
+ def download_video_from_url(url: str, output_path: str, video_file_name: str) -> str:
28
+ """Baixa o vídeo do YouTube usando yt-dlp."""
29
+ video_path = f'{output_path}/{video_file_name}.%(ext)s'
30
+ print(f"Baixando vídeo de {url} para {video_path}...")
31
+ try:
32
+ # Comando yt-dlp para baixar o melhor formato mp4
33
+ command = [
34
+ 'yt-dlp',
35
+ "--cookies", YOUTUBE_COOKIE_PATH,
36
+ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
37
+ '-o', video_path,
38
+ url
39
+ ]
40
+ result = subprocess.run(command, check=True, capture_output=True, text=True)
41
+ lista_arquivos = File_Util.retirar_sufixo_codec_arquivo(output_path)
42
+ print("Download de áudio concluído com sucesso.")
43
+ return f"{output_path}/{lista_arquivos[0]}"
44
+ except subprocess.CalledProcessError as e:
45
+ print(f"Erro ao baixar o vídeo: {e}")
46
+ print(f"Saída do erro: {e.stderr}")
47
+ return False
48
+ except FileNotFoundError:
49
+ print("Erro: O comando 'yt-dlp' não foi encontrado. Certifique-se de que ele está instalado e no PATH do sistema.")
50
+ print("Você pode instalá-lo com: pip install yt-dlp")
51
+ return False
52
+
53
+
54
+
55
+ def execute_python_code_tool(code_path: str) -> str:
56
+ """
57
+ Execute code python informed in code_path param
58
+
59
+ Args:
60
+ code_path: Path to the python file.
61
+
62
+ Returns:
63
+ Execution result.
64
+ """
65
+
66
+ saida = io.StringIO()
67
+ erros = io.StringIO()
68
+
69
+ final_code_path = File_Util.tratar_arquivo_local(code_path)
70
+ if not final_code_path:
71
+ return f"Erro: Arquivo não encontrado em {code_path}"
72
+ print(f"Executando código em {final_code_path}...")
73
+
74
+ try:
75
+ with open(final_code_path, 'r', encoding='utf-8') as f:
76
+ codigo = f.read()
77
+
78
+ # Captura stdout e stderr usando contexto
79
+ with redirect_stdout(saida), redirect_stderr(erros):
80
+ exec(codigo, {'__name__': '__main__'})
81
+
82
+ # Pega o conteúdo das saídas
83
+ saida_valor = saida.getvalue()
84
+ erro_valor = erros.getvalue()
85
+
86
+ if erro_valor:
87
+ return f"[ERRO DE EXECUÇÃO]:\n{erro_valor}"
88
+ return saida_valor if saida_valor.strip() else "[SEM SAÍDA]"
89
+
90
+ except Exception:
91
+ return f"[EXCEÇÃO DURANTE EXECUÇÃO]:\n{traceback.format_exc()}"
92
+
93
+
94
+
95
+ def chess_image_to_fen_tool(image_path:str, current_player: Literal["black", "white"]) -> Dict[str,str]:
96
+ """
97
+ Convert chess image to FEN (Forsyth-Edwards Notation) notation.
98
+ Args:
99
+ image_path: Path to the image file.
100
+ current_player: Whose turn it is to play. Must be either 'black' or 'white'.
101
+ Returns:
102
+ JSON with FEN (Forsyth-Edwards Notation) string representing the current board position.
103
+ """
104
+ print(f"Image to Fen invocada com os seguintes parametros:")
105
+ print(f"image_path: {image_path}")
106
+ print(f"current_player: {current_player}")
107
+
108
+
109
+ if current_player not in ["black", "white"]:
110
+ raise ValueError("current_player must be 'black' or 'white'")
111
+
112
+ base64_image = Image_Util.encode_image_to_base64(image_path)
113
+ if not base64_image:
114
+ raise ValueError("Failed to encode image to base64.")
115
+ base64_image_encoded = f"data:image/jpeg;base64,{base64_image}"
116
+ url = CHESSVISION_TO_FEN_URL
117
+ payload = {
118
+ "board_orientation": "predict",
119
+ "cropped": False,
120
+ "current_player": "black",
121
+ "image": base64_image_encoded,
122
+ "predict_turn": False
123
+ }
124
+
125
+ response = requests.post(url, json=payload)
126
+ if response.status_code == 200:
127
+ dados = response.json()
128
+ if dados.get("success"):
129
+ print(f"Retorno Chessvision {dados}")
130
+ fen = dados.get("result")
131
+ fen = fen.replace("_", " ") #retorna _ no lugar de espaço em branco
132
+ return json.dumps({"fen": fen})
133
+ else:
134
+ raise Exception("Requisição feita, mas falhou na predição.")
135
+ else:
136
+ raise Exception(f"Erro na requisição: {response.status_code}")
137
+
138
+ def chess_fen_get_best_next_move_tool(fen: str, current_player: Literal["black", "white"]) -> str:
139
+ """
140
+ Return the best move in algebric notation.
141
+ Args:
142
+ fen: FEN (Forsyth-Edwards Notation) notation.
143
+ Returns:
144
+ Best move in algebric notation.
145
+ """
146
+ if not fen:
147
+ raise ValueError("fen must be provided.")
148
+ if current_player not in ["black", "white"]:
149
+ raise ValueError("current_player must be 'black' or 'white'")
150
+
151
+
152
+ url = CHESS_MOVE_API
153
+ payload = {
154
+ "fen": fen,
155
+ "depth": 1
156
+ }
157
+
158
+ print(f"Buscando melhor jogada em {CHESS_MOVE_API} - {payload}")
159
+
160
+ response = requests.post(url, json=payload)
161
+ if response.status_code == 200:
162
+ #print(f"Retorno melhor jogada --> {response.text}")
163
+ dados = response.json()
164
+ move_algebric_notation = dados.get("san")
165
+ move = dados.get("text")
166
+ print(f"Melhor jogada segundo chess-api.com -> {move}")
167
+
168
+ return move_algebric_notation
169
+
170
+ else:
171
+ raise Exception(f"Erro na requisição: {response.status_code}")
172
+
173
+
174
+
175
+ def extract_frames_from_video_to_files(url: str) -> List[str]:
176
+ """
177
+ Extract frames from a video and store in temporaily files.
178
+ Args:
179
+ url: URL to the video.
180
+ Returns:
181
+ List of frame file paths.
182
+ """
183
+ frames_list: List[str] = []
184
+ File_Util.create_or_clear_output_directory(OUTPUT_VIDEO_PATH)
185
+ File_Util.create_or_clear_output_directory(OUTPUT_IMAGE_PATH)
186
+ video_download_file_name = Video_Util.download_video_from_url(url, OUTPUT_VIDEO_PATH, VIDEO_FILE_NAME)
187
+ if not video_download_file_name:
188
+ raise ValueError("Failed to download video.")
189
+
190
+
191
+ print(f"Extraindo frames de {video_download_file_name} a cada {FRAME_INTERVAL_SECONDS} segundos...")
192
+ if not os.path.exists(video_download_file_name):
193
+ print(f"Erro: Arquivo de vídeo não encontrado em {video_download_file_name}")
194
+ return []
195
+
196
+ cap = cv2.VideoCapture(video_download_file_name)
197
+ # Verificar a resolução
198
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
199
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
200
+ print(f"Resolução original do vídeo: {width}x{height}")
201
+
202
+ if not cap.isOpened():
203
+ print(f"Erro ao abrir o arquivo de vídeo: {video_download_file_name}")
204
+ return []
205
+
206
+ fps = cap.get(cv2.CAP_PROP_FPS)
207
+ if fps == 0:
208
+ print("Erro: Não foi possível obter o FPS do vídeo. Usando FPS padrão de 30.")
209
+ fps = 30 # Valor padrão caso a leitura falhe
210
+
211
+ # retirado para permitir fracionado frame_interval = int(fps * interval_sec)
212
+ frame_interval = fps * FRAME_INTERVAL_SECONDS
213
+
214
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
215
+ print(f"Vídeo FPS: {fps:.2f}, Intervalo de frames: {frame_interval}, Total de frames: {total_frames}")
216
+
217
+ extracted_frames_paths = []
218
+ frame_count = 0
219
+ saved_frame_index = 5 # o importante nunca começa no inicio, é um deslocamento inicial para iniciar depois da introdução
220
+
221
+
222
+ while True:
223
+ # Define a posição do próximo frame a ser lido
224
+ # Adiciona frame_interval para pegar o frame *após* o intervalo de tempo
225
+ # ajustado para float target_frame_pos = saved_frame_index * frame_interval
226
+ target_frame_pos = int(saved_frame_index * frame_interval)
227
+
228
+ if target_frame_pos >= total_frames:
229
+ break # Sai se o próximo frame alvo estiver além do final do vídeo
230
+
231
+ if (saved_frame_index < INICIO_FRAME_IMPORTANTE or saved_frame_index > FIM_FRAME_IMPORTANTE):
232
+ print(f"Pulando frame {saved_frame_index}")
233
+ saved_frame_index += 1
234
+ continue # evitar custo desnecessário para inferencia ao gpt
235
+ cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame_pos)
236
+ ret, frame = cap.read()
237
+
238
+ if not ret:
239
+ print(f"Não foi possível ler o frame na posição {target_frame_pos}. Pode ser o fim do vídeo ou um erro.")
240
+ break # Sai se não conseguir ler o frame
241
+
242
+ # redimensiona o frame (custo chamada)
243
+ # removido porque poderia afetar a nitidez e impactar o resultado
244
+ # frame = cv2.resize(frame, (1280, 720))
245
+
246
+ # Calcula o timestamp em segundos
247
+ timestamp_sec = target_frame_pos / fps
248
+
249
+ # Salva o frame
250
+ frame_filename = f"frame_{saved_frame_index:04d}_time_{timestamp_sec:.2f}s.png"
251
+ frame_path = os.path.join(OUTPUT_IMAGE_PATH, frame_filename)
252
+ try:
253
+ # modificado para salvar com qualidade máxima cv2.imwrite(frame_path, frame)
254
+ cv2.imwrite(frame_path, frame, [cv2.IMWRITE_PNG_COMPRESSION, 0])
255
+
256
+ extracted_frames_paths.append(frame_path)
257
+ print(f"Frame salvo: {frame_path} (Timestamp: {timestamp_sec:.2f}s)")
258
+ saved_frame_index += 1
259
+ except Exception as e:
260
+ print(f"Erro ao salvar o frame {frame_path}: {e}")
261
+ # Continua para o próximo intervalo mesmo se um frame falhar
262
+
263
+ # Segurança para evitar loop infinito caso algo dê errado com a lógica de posição
264
+ if saved_frame_index > (total_frames / frame_interval) + 2:
265
+ print("Aviso: N��mero de frames salvos parece exceder o esperado. Interrompendo extração.")
266
+ break
267
+
268
+ cap.release()
269
+ print(f"Extração de frames concluída. Total de frames salvos: {len(extracted_frames_paths)}")
270
+ return extracted_frames_paths
271
+
272
+
273
+ return frames_list;
274
+
275
+
276
+ def count_birds_species(image_path: str) -> int:
277
+
278
+ bird_count_prompt = """You are a world-class expert in avian species classification. Analyze the provided image and determine how many
279
+ **distinct bird species** are present. Consider size, shape, plumage, coloration, and beak structure. Focus only on
280
+ visible morphological differences. Return a **single integer** with no explanation. Do not count individuals of the same species. '
281
+ If unsure, assume that bird is a different specie."""
282
+
283
+ if not OPENAI_API_KEY:
284
+ raise ValueError("OPENAI API KEY must be defined.")
285
+
286
+ base64_image = Image_Util.encode_image_to_base64(image_path)
287
+
288
+ genai.configure(api_key=GEMINI_API_KEY)
289
+ model = genai.GenerativeModel(GEMINI_MODEL)
290
+ print(f"Enviando frame para análise no {GEMINI_MODEL}...")
291
+
292
+ try:
293
+ response = model.generate_content(
294
+ contents=[
295
+ {
296
+ "role": "user",
297
+ "parts": [
298
+ {f"text": f"{bird_count_prompt}"},
299
+ {"inline_data": {
300
+ "mime_type": "image/jpeg",
301
+ "data": base64_image
302
+ }}
303
+ ]
304
+ }
305
+ ],
306
+ generation_config={
307
+ "temperature": 0.0,
308
+ "max_output_tokens": 500
309
+ })
310
+
311
+ # Extrai o conteúdo da resposta
312
+ analysis_result = response.text.strip()
313
+ print(f"Análise recebida: {analysis_result}")
314
+
315
+ return int(analysis_result)
316
+ except Exception as e:
317
+ print(f"Erro ao chamar a API OpenAI: {e}")
318
+ return {"error": str(e)}
319
+
320
+ def bird_video_count_tool(url: str) -> int:
321
+ """
322
+ Count different species of birds in a video.
323
+ Args:
324
+ url: URL to the video.
325
+ Returns:
326
+ Count of different species of birds.
327
+ """
328
+
329
+
330
+
331
+ frames_path_list = extract_frames_from_video_to_files(url)
332
+ if not frames_path_list:
333
+ raise ValueError("Failed to extract frames.")
334
+
335
+ max_species: int = 0
336
+ for frame_path in frames_path_list:
337
+ species_count = count_birds_species(frame_path)
338
+ if species_count > max_species:
339
+ max_species = species_count
340
+
341
+ return max_species
342
+
343
+
344
+ def extract_text_from_url_tool (audio_url:str) -> str:
345
+ """
346
+ Extracts text from an audio url using the OpenAI Whisper API.
347
+ Args:
348
+ audio_url: URL to the audio file.
349
+ Returns:
350
+ text extracted from the audio url.
351
+ """
352
+
353
+ if not audio_url:
354
+ raise ValueError("'audio_url'must be provided.")
355
+ if not OUTPUT_AUDIO_PATH:
356
+ raise ValueError("OUTPUT_AUDIO_PATH must be defined.")
357
+
358
+ File_Util.create_or_clear_output_directory(OUTPUT_AUDIO_PATH)
359
+ audio_download_file_name = Audio_Util.download_audio_from_url(audio_url, OUTPUT_AUDIO_PATH, AUDIO_FILENAME)
360
+ if not audio_download_file_name:
361
+ raise ValueError("Failed to download audio.")
362
+
363
+ transcript = Audio_Util.extract_text_from_audio_file(audio_download_file_name)
364
+
365
+ return transcript
366
+
367
+
368
+ def extract_text_from_file_tool(audio_file_name:str) -> str:
369
+ """
370
+ Extracts text from an audio file using the OpenAI Whisper API.
371
+ Args:
372
+ audio_file_name: Name of the audio file.
373
+ Returns:
374
+ text extracted from the audio file.
375
+ """
376
+
377
+ if not audio_file_name and not audio_file_name:
378
+ raise ValueError(" 'audio_file_name' must be provided.")
379
+ if not OUTPUT_AUDIO_PATH:
380
+ raise ValueError("OUTPUT_AUDIO_PATH must be defined.")
381
+
382
+ treated_path = f"{AGENTS_FILES_PATH}/{audio_file_name}"
383
+ transcript = Audio_Util.extract_text_from_audio_file(treated_path)
384
+
385
+ return transcript
386
+
387
+
388
+
389
+ class Search_Web_Result(BaseModel):
390
+ page_title: str
391
+ page_url: str
392
+ page_html_content: str
393
+ page_markdown_content: str
394
+
395
+
396
+ def search_web_tool(query: str,
397
+ wikipedia_has_priority: bool,
398
+ wikipedia_historical_date: Optional[str]=None,
399
+ convert_to_markdown: bool=True
400
+ ) -> List[Search_Web_Result]:
401
+ """
402
+ Searches the web for pages with the most relevant information about the topic, returning a list of Search_Web_Result (title, url, html content and markdown content)
403
+
404
+ Args:
405
+ query: The main topic or question to search for.
406
+ use_wikipedia_priority: If true, prioritize results from Wikipedia.
407
+ wikipedia_date: Optional date to fetch historical Wikipedia data.
408
+
409
+ Returns:
410
+ A list of URLs or page titles sorted by relevance.
411
+ """
412
+
413
+ return_list: List[Search_Web_Result] = []
414
+
415
+
416
+ try:
417
+ tavily = TavilyClient(api_key=TAVILY_API_KEY)
418
+ except Exception as e:
419
+ print(f"Erro ao inicializar o cliente Tavily: {e}")
420
+ raise
421
+
422
+ print(f"\n--- Realizando busca por '{query}' usando Tavily ---")
423
+ print(f"Prioridade para Wikipedia: {wikipedia_has_priority}")
424
+ print(f"Data para Wikipedia: {wikipedia_historical_date}")
425
+ print(f"Convertendo HTML para Markdown: {convert_to_markdown}")
426
+
427
+ try:
428
+ response = tavily.search(query=query, search_depth="basic", max_results=10)
429
+ search_results = response.get('results', [])
430
+ except Exception as e:
431
+ print(f"Erro ao realizar busca com Tavily: {e}")
432
+ raise
433
+
434
+ if not search_results:
435
+ print("Nenhum resultado encontrado pela busca Tavily.")
436
+ return []
437
+
438
+ if wikipedia_has_priority:
439
+ print("Prioridade para Wikipedia habilitada. Filtrando resultados Tavily por Wikipedia...")
440
+ return _processa_resultado_wikipedia(search_results, wikipedia_historical_date, convert_to_markdown)
441
+
442
+
443
+ urls_to_process = []
444
+ print("Usando os 5 primeiros resultados gerais.")
445
+ urls_to_process = [res['url'] for res in search_results[:5]]
446
+
447
+ print(f"\n--- Processando {len(urls_to_process)} URLs selecionadas ---")
448
+ for url in urls_to_process:
449
+ title, html_content = Web_Util.download_html(url)
450
+ if not title or not html_content:
451
+ raise AssertionError(f"Falha ao processar URL: {url}")
452
+
453
+ md_content = ""
454
+ if convert_to_markdown:
455
+ md_content = Web_Util.convert_html_to_markdown(title, html_content)
456
+ if not md_content:
457
+ raise AssertionError(f"Falha ao converter URL: {url}, html:{html_content}")
458
+ return_list.append(Search_Web_Result(
459
+ page_title=title,
460
+ page_url=url,
461
+ page_html_content=html_content if not convert_to_markdown else "",
462
+ page_markdown_content=md_content
463
+ ))
464
+
465
+ return return_list
466
+
467
+
468
+
469
+ def _processa_resultado_wikipedia(search_results: List[str], wikipedia_historical_date: str,
470
+ convert_to_markdown:bool) -> List[Search_Web_Result]:
471
+ """
472
+ Trata do resultado de pesquisa quando existe prioridade para Wikipedia.
473
+ Args:
474
+ search_results: Lista com resultados da busca realizado pelo Tavily.
475
+ wikipedia_historical_date: A data para buscar uma revisão histórica da Wikipedia.
476
+ convert_to_markdown: Se true, converte o conteúdo HTML para Markdown.
477
+ Returns:
478
+ Lista com os resultados processados.
479
+ """
480
+
481
+ print("Prioridade para Wikipedia habilitada. Filtrando resultados Tavily por Wikipedia...")
482
+ wiki_urls = [res['url'] for res in search_results if Web_Util.is_wikipedia_url(res['url'])]
483
+ if not wiki_urls:
484
+ print("Nenhuma URL da Wikipedia encontrada nos resultados.")
485
+ return []
486
+ # Pega o primeiro resultado da Wikipedia
487
+ first_wiki_url = wiki_urls[0]
488
+ page_title_guess = first_wiki_url.split('/')[-1].replace('_', ' ')
489
+ page_check = Wikipedia_Util.wiki_executor.page(page_title_guess)
490
+ if not page_check.exists():
491
+ raise AssertionError(f"Página '{page_title_guess}' não encontrada na Wikipedia.")
492
+
493
+ page_title = None
494
+ page_url = None
495
+
496
+ if not wikipedia_historical_date:
497
+ page_title = page_title_guess
498
+ page_url = first_wiki_url
499
+ else:
500
+ # Busca revisão histórica
501
+ historical_wiki_info: Wikipedia_Historical_Page = Wikipedia_Util.get_wikipedia_page_historical_content(page_check.title, wikipedia_historical_date)
502
+ print(f"Dados da versão histórica wikipedia - {historical_wiki_info}")
503
+ page_title = historical_wiki_info.title
504
+ page_url = historical_wiki_info.url
505
+
506
+ title, html_content = Web_Util.download_html(page_url)
507
+ print(f"title {title}")
508
+ if not html_content:
509
+ raise AssertionError(f"Conteúdo da página {page_url} não foi baixado, não será possível continuar.")
510
+
511
+ md_content = ""
512
+ if convert_to_markdown:
513
+ md_content = Web_Util.convert_html_to_markdown(page_title, html_content)
514
+ if md_content and wikipedia_historical_date:
515
+ # Adiciona informação sobre a revisão no início do conteúdo (CORRIGIDO)
516
+ header = f"# Wikipedia Content for '{historical_wiki_info.title}'\n"
517
+ header += f"*Revision from {historical_wiki_info.timestamp} (ID: {historical_wiki_info.revision_id})*\n"
518
+ header += f"*Regarding search date: {wikipedia_historical_date}*\n\n"
519
+ header += "---\n\n"
520
+ md_content = header + md_content
521
+
522
+ return_list = [
523
+ Search_Web_Result(
524
+ page_title=page_title,
525
+ page_url=page_url,
526
+ page_html_content=html_content if not convert_to_markdown else "",
527
+ page_markdown_content=md_content
528
+ )
529
+ ]
530
+ return return_list
531
+
532
+
533
+
534
+ def text_inverter_tool(text: str ) -> str:
535
+ """
536
+ Invert the text.
537
+ Args:
538
+ text: Text to be inverted.
539
+ Returns:
540
+ Inverted text.
541
+ """
542
+ return text[::-1]
543
+
544
+
545
+
546
+ def parse_markdown_table_to_dict(markdown: str) -> dict:
547
+ """
548
+ Convert binary operation table in markdown format to a dictionary
549
+ Args:
550
+ markdown: table in markdown format
551
+
552
+ """
553
+ linhas = markdown.strip().split('\n')
554
+
555
+ # Remove barras verticais nas extremidades e divide pelas internas
556
+ cabecalho = [col.strip() for col in linhas[0].strip('|').split('|')]
557
+ colunas = cabecalho[1:] # ignora o '*'
558
+
559
+ tabela = {}
560
+
561
+ for linha in linhas[2:]: # pula cabeçalho e separador
562
+ partes = [p.strip() for p in linha.strip('|').split('|')]
563
+ linha_elem = partes[0]
564
+ valores = partes[1:]
565
+ if len(valores) != len(colunas):
566
+ raise ValueError(f"Erro ao processar linha '{linha_elem}': número de colunas incompatível.")
567
+ tabela[linha_elem] = dict(zip(colunas, valores))
568
+
569
+ return tabela
570
+
571
+ def check_table_commutativity_tool(markdown: str) -> dict:
572
+ """
573
+ Check if the table in markdown format is commutative
574
+ Args:
575
+ table: table in markdown format
576
+ """
577
+ contraexemplos = []
578
+ elementos = set()
579
+
580
+ table = parse_markdown_table_to_dict(markdown)
581
+
582
+ for x in table:
583
+ for y in table:
584
+ if x != y and table[x][y] != table[y][x]:
585
+ contraexemplos.append((x, y))
586
+ elementos.update([x, y])
587
+ return {
588
+ "counter_example": contraexemplos,
589
+ "elements_involved": sorted(elementos)
590
+ }
591
+
592
+
593
+
594
+ def get_excel_columns_tool(file_path: str) -> list[str]:
595
+ """
596
+ Get the columns of an Excel file.
597
+
598
+ Args:
599
+ file_path: Path to the Excel file.
600
+
601
+ Returns:
602
+ List of column names.
603
+
604
+ """
605
+ final_excel_path = File_Util.tratar_arquivo_local(file_path)
606
+ print(f"Extraindo as colunas do arquivo {file_path}")
607
+
608
+ df = pd.read_excel(final_excel_path, nrows=0)
609
+ return df.columns.tolist()
610
+
611
+ def calculate_excel_sum_by_columns_tool(
612
+ file_path: str,
613
+ include_columns: list[str]
614
+ ) -> str:
615
+ """
616
+ Calculate the sum of values in specified columns of an Excel file.
617
+
618
+ Args:
619
+ - file_path: Path to the Excel file.
620
+ - include_columns: Columns included in the sum
621
+ """
622
+ final_excel_path = File_Util.tratar_arquivo_local(file_path)
623
+ print(f"Calculando soma de {include_columns} em {final_excel_path}")
624
+
625
+ df = pd.read_excel(final_excel_path)
626
+ total = df[include_columns].sum().sum() # soma todas as colunas e depois soma os totais
627
+ return total
video_util.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ from constantes import YOUTUBE_COOKIE_PATH
3
+ from file_util import File_Util
4
+
5
+
6
+ class Video_Util:
7
+ def download_video_from_url(url: str, output_path: str, video_file_name: str) -> str:
8
+ """Baixa o vídeo do YouTube usando yt-dlp."""
9
+ video_path = f'{output_path}/{video_file_name}.%(ext)s'
10
+ print(f"Baixando vídeo de {url} para {video_path}...")
11
+ try:
12
+ # Comando yt-dlp para baixar o melhor formato mp4
13
+ command = [
14
+ 'yt-dlp',
15
+ "--cookies", YOUTUBE_COOKIE_PATH,
16
+ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
17
+ '-o', video_path,
18
+ url
19
+ ]
20
+ result = subprocess.run(command, check=True, capture_output=True, text=True)
21
+ lista_arquivos = File_Util.retirar_sufixo_codec_arquivo(output_path)
22
+ print("Download de áudio concluído com sucesso.")
23
+ return f"{output_path}/{lista_arquivos[0]}"
24
+ except subprocess.CalledProcessError as e:
25
+ print(f"Erro ao baixar o vídeo: {e}")
26
+ print(f"Saída do erro: {e.stderr}")
27
+ return False
28
+ except FileNotFoundError:
29
+ print("Erro: O comando 'yt-dlp' não foi encontrado. Certifique-se de que ele está instalado e no PATH do sistema.")
30
+ print("Você pode instalá-lo com: pip install yt-dlp")
31
+ return False
web_util.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+ from bs4 import BeautifulSoup
3
+ import markdownify
4
+ import requests
5
+
6
+
7
+ class Web_Util:
8
+ HEADERS = {
9
+ 'User-Agent': 'MyCoolSearchBot/1.0 ([email protected])'
10
+ }
11
+
12
+ @staticmethod
13
+ def is_wikipedia_url(url: str) -> bool:
14
+ """Verifica se uma URL pertence ao domínio da Wikipedia."""
15
+ return "wikipedia.org" in url.lower()
16
+
17
+ @staticmethod
18
+ def _limpar_html(html: str) -> Tuple[str, str]:
19
+ """
20
+ Remove tags <script>, <style> e atributos inline.
21
+ Args:
22
+ html: HTML a ser limpo.
23
+ Returns:
24
+ Titulo da pagina e html limpo.
25
+
26
+ """
27
+
28
+ soup = BeautifulSoup(html, 'html.parser')
29
+
30
+ # Extrai o título da página (primeiro tenta <title>, depois <h1>)
31
+ title_tag = soup.find('title')
32
+ title = title_tag.get_text(strip=True) if title_tag else None
33
+
34
+ # Remove tags <script> e <style>
35
+ for tag in soup(['script', 'style']):
36
+ tag.decompose()
37
+ # Remove tags <img>
38
+ for img in soup.find_all('img'):
39
+ img.decompose()
40
+ # Remove atributos que aplicam CSS ou JS inline
41
+ for tag in soup.find_all(True):
42
+ for attr in ['style', 'onclick', 'onmouseover', 'onload', 'class', 'id']:
43
+ if attr in tag.attrs:
44
+ del tag.attrs[attr]
45
+
46
+ return title, str(soup)
47
+
48
+ @staticmethod
49
+ def download_html(url: str) -> Tuple[str, str]:
50
+ """
51
+ Baixa o conteúdo HTML de uma URL, retornando também o titulo.
52
+ Args:
53
+ url: URL a ser baixada.
54
+ Returns:
55
+ Uma tupla contendo o título e o conteúdo HTML.
56
+ """
57
+ print(f"Baixando e convertendo: {url}")
58
+ try:
59
+ response = requests.get(url, headers=Web_Util.HEADERS, timeout=20)
60
+ response.raise_for_status() # Verifica se houve erro no request
61
+ # Tenta detectar a codificação, mas assume UTF-8 como fallback
62
+ response.encoding = response.apparent_encoding or 'utf-8'
63
+ html_content = response.text
64
+ # Usa readability para extrair o conteúdo principal
65
+ title, cleaned_html = Web_Util._limpar_html(html_content)
66
+
67
+ return title, cleaned_html
68
+
69
+ except requests.exceptions.RequestException as e:
70
+ print(f"Erro ao acessar a URL (requestException) {url}: {e}")
71
+ return None
72
+ except Exception as e:
73
+ print(f"Erro ao acessar a URL (erro genérico) {url}: {e}")
74
+ return None
75
+
76
+ @staticmethod
77
+ def convert_html_to_markdown(title: str, html: str) -> str:
78
+ """Converte o html para markdown."""
79
+ try:
80
+ md_content = markdownify.markdownify(
81
+ html,
82
+ heading_style="ATX",
83
+ strip=['script', 'style'],
84
+ escape_underscores=False)
85
+
86
+ return f"# {title}\n\n" + md_content.strip()
87
+ except Exception as e:
88
+ print(f"Erro ao converter HTML para Markdown: {e}")
89
+ return None
90
+
91
+ @staticmethod
92
+ def download_html_and_convert_to_md(url: str) -> str:
93
+ """Baixa o conteúdo HTML de uma URL e o converte para Markdown."""
94
+ title, html = Web_Util.download_html(url)
95
+ if title and html:
96
+ return Web_Util.convert_html_to_markdown(title)
97
+ else:
98
+ return None
99
+
wikipedia_util.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timezone
2
+ from typing import Tuple
3
+ import urllib
4
+
5
+ from gradio import List
6
+ from pydantic import BaseModel
7
+ import requests
8
+ import wikipediaapi
9
+
10
+
11
+ class Wikipedia_Historical_Page(BaseModel):
12
+ title: str
13
+ url: str
14
+ revision_id: str
15
+ timestamp: str
16
+
17
+
18
+ class Wikipedia_Util:
19
+
20
+ WIKI_LANG = 'en' # Linguagem da Wikipedia (atualizado para inglês)
21
+ MEDIAWIKI_API_URL = f"https://{WIKI_LANG}.wikipedia.org/w/api.php"
22
+ HEADERS = {
23
+ 'User-Agent': 'MyCoolSearchBot/1.0 ([email protected])'
24
+ }
25
+ wiki_executor = wikipediaapi.Wikipedia(
26
+ language=WIKI_LANG,
27
+ extract_format=wikipediaapi.ExtractFormat.HTML, # Usado apenas para page.exists()
28
+ user_agent='MyCoolSearchBot/1.0 ([email protected])' # Definir um User-Agent é boa prática
29
+ )
30
+
31
+ @staticmethod
32
+ def get_wikipedia_revision_info(page_title: str, target_date_str: str) -> Tuple[str,str]:
33
+ """
34
+ Busca o ID e timestamp da revisão mais próxima (<=) da data fornecida via API MediaWiki.
35
+
36
+ Args:
37
+ page_title: wikipedia encontra páginas históricas pelo titulo
38
+ target_date_str: data no formato "YYYY-MM-DD"
39
+
40
+ Returns:
41
+ Uma tupla contendo o ID da revisão e o timestamp da revisão.
42
+ """
43
+ try:
44
+ # Converte a data string para um objeto datetime e formata para ISO 8601 com Z (UTC)
45
+ target_dt = datetime.strptime(target_date_str, '%Y-%m-%d')
46
+ # Precisamos do final do dia para garantir que incluímos todas as revisões daquele dia
47
+ target_dt_end_of_day = target_dt.replace(hour=23, minute=59, second=59, tzinfo=timezone.utc)
48
+ target_timestamp_iso = target_dt_end_of_day.strftime('%Y-%m-%dT%H:%M:%SZ')
49
+ except ValueError:
50
+ print("Formato de data inválido. Use AAAA-MM-DD.")
51
+ return None, None
52
+
53
+ params = {
54
+ "action": "query",
55
+ "prop": "revisions",
56
+ "titles": page_title,
57
+ "rvlimit": 1,
58
+ "rvdir": "older", # Busca a revisão imediatamente anterior ou igual ao timestamp
59
+ "rvprop": "ids|timestamp", # Queremos o ID da revisão e o timestamp
60
+ "rvstart": target_timestamp_iso, # Começa a busca a partir desta data/hora
61
+ "format": "json",
62
+ "formatversion": 2 # Formato JSON mais moderno e fácil de parsear
63
+ }
64
+
65
+ try:
66
+ print(f"Consultando API MediaWiki para revisão de '{page_title}' em {target_date_str}...")
67
+ response = requests.get(Wikipedia_Util.MEDIAWIKI_API_URL, params=params, headers=Wikipedia_Util.HEADERS, timeout=15)
68
+ response.raise_for_status()
69
+ data = response.json()
70
+
71
+ # Verifica se a página foi encontrada
72
+ page_data = data.get("query", {}).get("pages", [])
73
+ if not page_data or page_data[0].get("missing", False):
74
+ print(f"Página '{page_title}' não encontrada na API MediaWiki.")
75
+ # Tenta verificar com a biblioteca wikipediaapi como fallback (pode pegar redirecionamentos)
76
+ page = Wikipedia_Util.wiki_executor.page(page_title)
77
+ if page.exists():
78
+ print(f"Página '{page_title}' existe (possivelmente redirecionada para '{page.title}'). Tentando novamente com o título canônico...")
79
+ return Wikipedia_Util.get_wikipedia_revision_info(page.title, target_date_str) # Chama recursivamente com o título correto
80
+ else:
81
+ print(f"Página '{page_title}' realmente não encontrada.")
82
+ return None, None
83
+
84
+ # Extrai as revisões
85
+ revisions = page_data[0].get("revisions", [])
86
+ if not revisions:
87
+ print(f"Nenhuma revisão encontrada para '{page_title}' antes ou em {target_date_str}.")
88
+ # Pode acontecer se a página foi criada depois da data alvo
89
+ return None, None
90
+
91
+ revision = revisions[0]
92
+ revid = revision.get("revid")
93
+ timestamp = revision.get("timestamp")
94
+ print(f"Encontrada revisão: ID={revid}, Timestamp={timestamp}")
95
+ return revid, timestamp
96
+
97
+ except requests.exceptions.RequestException as e:
98
+ print(f"Erro ao chamar a API MediaWiki: {e}")
99
+ return None, None
100
+ except Exception as e:
101
+ print(f"Erro ao processar resposta da API MediaWiki: {e}")
102
+ return None, None
103
+
104
+
105
+ @staticmethod
106
+ def get_wikipedia_page_historical_content(page_title: str, target_date_str: str) -> List[Wikipedia_Historical_Page]:
107
+ """Obtém o conteúdo Markdown de uma revisão histórica específica da Wikipedia."""
108
+
109
+ # Busca o ID da revisão correta usando a API MediaWiki
110
+ revid, timestamp = Wikipedia_Util.get_wikipedia_revision_info(page_title, target_date_str)
111
+
112
+ if not revid:
113
+ print(f"Não foi poss��vel encontrar uma revisão adequada para '{page_title}' em {target_date_str}.")
114
+ return None
115
+
116
+ # Constrói a URL para a revisão específica
117
+ # Nota: Codifica o título da página para a URL
118
+ # Precisamos garantir que estamos usando o título correto (pode ter sido redirecionado)
119
+ page_check = Wikipedia_Util.wiki_executor.page(page_title) # Verifica novamente para obter o título canônico se houve redirecionamento
120
+ if not page_check.exists():
121
+ print(f"Erro inesperado: Página '{page_title}' não encontrada após busca de revisão.")
122
+ return None
123
+ canonical_title = page_check.title
124
+ encoded_title = urllib.parse.quote(canonical_title.replace(' ', '_'))
125
+ revision_url = f"https://{Wikipedia_Util.WIKI_LANG}.wikipedia.org/w/index.php?title={encoded_title}&oldid={revid}"
126
+
127
+ return Wikipedia_Historical_Page(
128
+ title=canonical_title,
129
+ url=revision_url,
130
+ revision_id=str(revid), #parametro obrigatoriamente string
131
+ timestamp=timestamp
132
+ )
133
+